Data.table: fcase / case_when функция для data.table

Созданный на 5 сент. 2019  ·  29Комментарии  ·  Источник: Rdatatable/data.table

Связанные / продолжение # 3657

case_when - это распространенный инструмент в SQL, например, для создания меток на основе условий флагов, например, для вырезания возрастных групп:

  case when age < 18 then '0-18'
       when age < 35 then '18-35'
       when age < 65 then '35-65'
       else '65+'
  end as age_label

Наши товарищи из dplyr реализовали это как case_when с таким интерфейсом, как

dplyr::case_when(
  age < 18 ~ '0-18',
  age < 35 ~ '18-35',
  age < 65 ~ '35-65',
  TRUE ~ '65+'
)

Использование & интерполированных формул кажется довольно естественным для R - единственное, что я могу придумать, - это синтаксис on ( case_when('age<18' = '0-18', ...) ).

Что касается бэкэнда, dplyr выполняет двухпроходный цикл for на уровне R, который, например, требует оценки age < 65 для _все_ строк (тогда как это не нужно для чего-либо с метки '0-18' или '18-35' ).

Думаю, мы можем добиться большего с реализацией C. Будет интересно сравнить правильную ленивую реализацию с параллельной версией (поскольку IINM, выполняющий это параллельно, потребует сначала оценки всех значений "LHS", например, как Ян недавно встретился в frollapply ).

Обратите внимание, что обычно я делаю вещи case_when с поисковым соединением, но не так просто реализовать case_when как соединение _ в общем_ - хотя я думаю, что они изоморфны, поддерживая определить, каким должно быть соответствующее соединение, я думаю, это сложная проблема. например, в приведенном здесь примере порядок значений истинности имеет значение, поскольку в последующих условиях подразумевается (x & !any(y)) для каждого условия x , которому предшествовали условия y . Этот случай легко преобразовать в соединение для cut(age) , возможно, просто используя roll , но все может быть намного сложнее, когда несколько переменных участвуют в условиях истинности. Поэтому я не думаю, что этот путь будет плодотворным.

feature request

Самый полезный комментарий

Мы никуда не торопимся. Я думаю, что лень должна иметь решающее значение, иначе она не принесет ничего нового (кроме API) по сравнению с использованием таблицы поиска. Не пытайтесь решить все на первой итерации. Когда будете готовы, отправьте PR, чтобы получить обратную связь. Окончательное состояние может потребовать нескольких итераций или последующих PR.

Все 29 Комментарий

На самом деле я предпочитаю синаткс вроде fcase(test1, value1, test2, value2, ..., default) . Легче читать, когда tests и values довольно сложны.

Определенно, вероятно, будет проще кодировать из этого.

Один из способов продвинуться вперед - это использовать S3 и создать вызов (test1, value1, ....) для отправки для fcase.formula (лично я считаю, что формула читается более естественно)

Да, это легче читать, особенно когда test & value короткие и встроенные. Но я не уверен, что использование стиля формулы будет иметь «значительные» накладные расходы или нет, поскольку требует дополнительных исправлений. Если нет, то меня устраивают оба стиля ...

Вопрос, как это улучшает (или отличается от) субподчинение по ссылке? Вы можете сделать эквивалент case, используя следующее:

DT <- data.table(age = 0:100)
DT[, age_label := "65+"]
DT[age < 65, age_label := "35-65"]
DT[age < 35, age_label := "18-35"]
DT[age < 18, age_label := "0-18"]

для которых мы уже получаем автоматическую индексацию по умолчанию.

Хью, см. Последний абзац. У меня было написано более полное объяснение, но оно исчезло, когда я нажал «Комментарий» 😢

Ваш пример - частный случай. fcase должен быть более общим и не ограничиваться использованием в объекте данных.

Я думаю, что было бы более эффективно, если бы нам не нужно было оценивать все значение «LHS», но мне нужно подумать, как бы я решил эту проблему. Что вы думаете о таком?
fcase(variable, default, test1, value1,...,testN, valeN)

@ 2005m Я не совсем понимаю интуицию сигнатуры функции?

У меня есть исследовательская работа над веткой fcase , R API:

fcase = function(..., default=NA) .External(Cfcase, ..., default)

Я думаю, что сначала нужно реализовать предложение @shrektan, поскольку это будет довольно просто (просто нужно выяснить, как бороться с DOTSXP в C 😅; примечание .External необходимо принять ... ).

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

Я думаю, что мы можем передать объект списка в C. например,

fcase = function(..., default = NA) {
  .Call(Cfcase, list(...), default = NA)
}
SEXP fcase(SEXP x, SEXP default) {
  ...
}

Кроме того, ?.External говорит, что принято ... до 65:

... аргументы, передаваемые в скомпилированный код. До 65 для .Call.

fcase (переменная, по умолчанию, test1, значение1, ..., testN, valeN)

С помощью этой конструкции можно проверить отсутствие (.. 1) на уровне R, тогда
рекурсивно оценить fcase.

Пт, 13 сентября 2019 г., 12:31, Xianying Tan [email protected]
написал:

Я думаю, что мы можем передать объект списка в C. например,

fcase = function (..., по умолчанию = NA) {
.Call (Cfcase, list (...), по умолчанию = NA)
}

SEXP fcase (SEXP x, SEXP по умолчанию) {
...
}

-
Вы получили это, потому что прокомментировали.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/Rdatatable/data.table/issues/3823?email_source=notifications&email_token=AB54MDCWWKJSFOVMDDQWBADQJL3PDA5CNFSM4IT6NOD2YY3PNVWWK3TUL52HS4DFMVREXW3TUL52HS4DFMVREXW3TUL52HS4DFMVREXGW3
или отключить поток
https://github.com/notifications/unsubscribe-auth/AB54MDA2AHRQQRGQFC2FW73QJL3PDANCNFSM4IT6NODQ
.

@MichaelChirico , я думал использовать его вот так:
fcase(age, '65+', '<18', '0-18', '<35', '18-35')

поскольку он зависит только от одной переменной age , возможно, он подходит для fswitch() в качестве оболочки для fcase() ?

чем меньше проверок на уровне R, тем лучше. Я также предпочитаю .Call а не .External .

Сначала мы должны согласовать API. Как сказал @shrektan, fswitch может быть оболочкой для fcase , аналогично fifelse , если мы все согласны с этим, мы можем сосредоточиться на fcase API.
Как только API будет установлен, может быть, прототип R? это будет для 1.13.0, так что времени достаточно.

На данный момент у нас есть

  1. стандартный оценочный интерфейс
fcase(...)
fcase(..., default)
fcase(when1, value1, ..., default)
fcase(age < 18, '0-18', age < 35, '18-35', age < 65, '35-65', '65+')
  1. Интерфейс формулы NSE
fcase(...)
fcase(..., default)
fcase(x, ..., default)
fcase(age < 18 ~ '0-18', age < 35 ~ '18-35', age < 65 ~ '35-65', TRUE ~ '65+')
  1. "векторизованный"
fcase(when, value, default)
fcase(list(age < 18, age < 35, age < 65),
      list('0-18', '18-35', '35-65'),
      '65+')

любые другие предложения?

Немного пищи для размышлений:

https://coolbutuseless.github.io/2018/09/06/strict-case_when/

Автор этого сообщения, кажется, просит справочную таблицу. Проблему мы решаем обновлением при присоединении. Случай, когда у него другая цель, как описано в комментариях выше.

Вопрос: Может ли value1 иметь ту же длину, что и when1 или всегда равно 1?
Также я думаю, что нам нужно решить, какой из приведенных выше функциональных интерфейсов вам нужен, прежде чем что-либо кодировать? Пока я думаю, что есть другой способ кодирования. Некоторые простые (в том числе оценка каждого когда) и некоторые более сложные (в том числе не оценка каждого когда) ...

в SQL это обычно одинаковая длина (например, value1 и when1 являются столбцами), поэтому я ожидал, что это сработает.

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

Его можно найти в очень развивающемся пакете tidyfast адресу https://github.com/TysonStanley/tidyfast. Будет ли полезен этот очень простой подход?

Спасибо @TysonStanley . На самом деле я закончил писать функцию на C. Я надеялся сегодня вечером сделать запрос на перенос. Мне просто нужно закончить писать тесты. Я посмотрю на вашу функцию.

@ 2005m Это dplyr::case_when() ?

И да, взгляните и дайте мне знать, что вы думаете. Это довольно простой подход, поскольку я могу положиться на fifelse() .

Вот краткий обзор:

x = sample(1:100,3e7,replace = TRUE) # 114 Mb
data.table::setDTthreads(1L)

microbenchmark::microbenchmark(
dplyr::case_when(
    x < 10L ~ 0L,
    x < 20L ~ 10L,
    x < 30L ~ 20L,
    x < 40L ~ 30L,
    x < 50L ~ 40L,
    x < 60L ~ 50L,
    x > 60L ~ 60L
),
tidyfast::dt_case_when(
  x < 10L ~ 0L,
  x < 20L ~ 10L,
  x < 30L ~ 20L,
  x < 40L ~ 30L,
  x < 50L ~ 40L,
  x < 60L ~ 50L,
  x > 60L ~ 60L
),
data.table::fcase(
  x < 10L, 0L,
  x < 20L, 10L,
  x < 30L, 20L,
  x < 40L, 30L,
  x < 50L, 40L,
  x < 60L, 50L,
  x > 60L, 60L
),
times = 5L)
# Unit: seconds
#                    expr    min    lq  mean  median    uq    max neval
# dplyr::case_when         11.69 11.80 11.83   11.81  11.92 11.94     5
# tidyfast::dt_case_when    2.18  2.23  2.32    2.38   2.39  2.41     5
# data.table::fcase         1.87  1.91  2.02    2.02   2.05  2.26     5

Синтаксис отличается от dplyr::case_when но мы, вероятно, сможем его изменить.
Обратите внимание, мой компьютер очень старый. Код компилируется с флагом gcc 4.9 -02.
Функция будет поддерживать interger64 и nanotime.
Однако мне все еще нужно над этим поработать. Я думаю, что пиара сегодня не будет.

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

Планируете ли вы в будущем поддерживать другие векторные типы?

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

@jangorecki , да, он оценивает все случаи, и поэтому я не доволен. Я тоже не доволен выступлением. Если это не может быть улучшено, я думаю, что подход @TysonStanley лучше, потому что он проще и сроки аналогичны.

@TysonStanley , я тоже предпочитаю формулы, но это, наверное, субъективно ... Насчет других векторных типов я не знаю. Это зависит от команды.

Мы никуда не торопимся. Я думаю, что лень должна иметь решающее значение, иначе она не принесет ничего нового (кроме API) по сравнению с использованием таблицы поиска. Не пытайтесь решить все на первой итерации. Когда будете готовы, отправьте PR, чтобы получить обратную связь. Окончательное состояние может потребовать нескольких итераций или последующих PR.

Выполняет ли fifelse ленивое вычисление (т.е. только те случаи, которые ему необходимы)? Мне нужно взглянуть на код, но я подумал, что спрошу.

Да

fifelse(TRUE, 1, stop("a"))
#Error in fifelse(TRUE, 1, stop("a")) : a

@jangorecki , я надеюсь, что смогу поделиться своим кодом в эти выходные. Мне нужно написать файл Rd и добавить больше тестов.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги