Отправка `rbind` и `cbind` для `data.frame`

Задний план

Механизм диспетчеризации функций R rbind() и cbind() нестандартен. Я исследовал некоторые возможности написания функций rbind.myclass() или cbind.myclass(), когда одним из аргументов является data.frame, но пока у меня нет удовлетворительного подхода. Этот пост посвящен rbind, но то же самое относится и к cbind.

Проблема

Давайте создадим функцию rbind.myclass(), которая просто выдает эхо при вызове.

rbind.myclass <- function(...) "hello from rbind.myclass"

Мы создаем объект класса myclass, и все следующие вызовы класса rbind правильно направляются в класс rbind.myclass().

a <- "abc"
class(a) <- "myclass"
rbind(a, a)
rbind(a, "d")
rbind(a, 1)
rbind(a, list())
rbind(a, matrix())

Однако, когда один из аргументов (это не обязательно должен быть первый), rbind() вместо этого вызовет base::rbind.data.frame():

rbind(a, data.frame())

Такое поведение немного удивительно, но на самом деле оно задокументировано в разделе dispatch документа rbind(). Совет дан там такой:

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

На практике этот совет может оказаться трудновыполнимым. Преобразование во фрейм данных может удалить важную информацию о классе. Кроме того, пользователь, который может не знать о совете, может столкнуться с ошибкой или неожиданным результатом после ввода команды rbind(a, x).

подходы

Предупредить пользователя

Первая возможность состоит в том, чтобы предупредить пользователя о том, что вызов rbind(a, x) не должен выполняться, когда x является фреймом данных. Вместо этого пользователь пакета mypackage должен сделать явный вызов скрытой функции:

mypackage:::rbind.myclass(a, x)

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

Перехват rbind

В качестве альтернативы я попытался защитить пользователя, перехватив диспетчер. Моей первой попыткой было дать локальное определение base::rbind.data.frame():

rbind.data.frame <- function(...) "hello from my rbind.data.frame"
rbind(a, data.frame())
rm(rbind.data.frame)

Это не удается, так как rbind() не обманывается, вызывая rbind.data.frame из .GlobalEnv, и, как обычно, вызывает версию base.

Другая стратегия заключается в переопределении rbind() локальной функцией, которая была предложена в S3 диспетчеризация `rbind ` и `cbind`.

rbind <- function (...) {
  if (attr(list(...)[[1]], "class") == "myclass") return(rbind.myclass(...))
  else return(base::rbind(...))
}

Это прекрасно работает для отправки на rbind.myclass(), так что теперь пользователь может ввести rbind(a, x) для любого типа объекта x.

rbind(a, data.frame())

Минус в том, что после library(mypackage) мы получаем сообщение The following objects are masked from ‘package:base’: rbind.

Хотя технически все работает так, как ожидалось, должны быть способы получше, чем переопределение функции base.

Заключение

Ни один из вышеперечисленных вариантов не является удовлетворительным. Я читал об альтернативах, использующих диспетчеризацию S4, но пока не нашел никаких реализаций этой идеи. Любая помощь или указатели?


person Stef van Buuren    schedule 25.12.2017    source источник
comment
Насколько я знаю, более умные люди, чем я, такие как Мэтт Доул и Хэдли Уикхэм, до сих пор не смогли решить эту проблему элегантным способом. Вы можете изучить их обходные пути в источниках пакетов data.table или tidyverse (может быть, dplyr?).   -  person Roland    schedule 25.12.2017
comment
Спасибо. Это полезно. Я посмотрел, что сделано в data.table. Когда пользователь выдает library(data.table), пакет переопределяет base::rbind.data.frame для отправки внутреннему методу. Это работает для data.table, но я боюсь, что принцип, делающий это одновременно в нескольких пакетах, вызывает проблемы. В FAQ 2.23 Мэтт говорит: Если есть лучшее решение, мы с радостью его изменим.   -  person Stef van Buuren    schedule 25.12.2017
comment
Боюсь, мы врежемся в стену. Хэдли называет это неисправимым 56529411 и, кажется, сдался после долгих попыток. Так что и здесь нет решения...   -  person Stef van Buuren    schedule 25.12.2017
comment
Если у вас есть реальный вариант использования, я бы предложил поднять его в списке рассылки R-devel. На мой взгляд, это недостаток дизайна, который должен быть исправлен в R. Однако нет гарантии, что это будет исправлено. Особенно, если вы не поставляете патч.   -  person Roland    schedule 25.12.2017
comment
Это вариант, но я уверен, что есть причины, по которым R работает так, как он работает. Я поднял этот вопрос в надежде найти альтернативу.   -  person Stef van Buuren    schedule 26.12.2017
comment
То, что было хорошей причиной более десяти лет назад, не обязательно является хорошей причиной сегодня. Я подозреваю, что отправка rbind является наследием очень ранних версий R. Вероятно, это было реализовано для ускорения отправки S3 для rbind. Однако нужно иметь возможность отправлять не-data.frame метод для data.frames, которые имеют другой класс в качестве первого класса. Подозреваю, что такой сценарий тогда не рассматривался.   -  person Roland    schedule 26.12.2017


Ответы (3)


Как вы сами упомянули, использование S4 было бы хорошим решением, которое прекрасно работает. В последнее время я не исследовал кадры данных, поскольку меня гораздо больше интересуют другие обобщенные матрицы, как в моих давних пакетах CRAN «Матрица» («рекомендуется», т. Е. Часть каждого дистрибутива R), так и в «Rmpfr».

На самом деле даже двумя разными способами:
1) Rmpfr использует новый способ определения методов для '...' в rbind()/cbind(). это хорошо задокументировано в ?dotsMethods (мнемоника: '...' = точки) и реализовано в Rmpfr/R/array.R, строка 511 и далее (например, https://r-forge.r-project.org/scm/viewvc.php/pkg/R/array.R?view=annotate&root=rmpfr)

2) Matrix использует старый подход, определяя (S4) методы для rbind2() и cbind2(): Если вы читаете ?rbind, там упоминается об этом и когда используются rbind2/cbind2. Идея здесь: "2" означает, что вы определяете методы S4 с сигнатурой для двух ("2") матричных объектов, а rbind/cbind рекурсивно использует их для двух своих потенциально многих аргументов.

person Martin Mächler    schedule 27.12.2017
comment
Спасибо. Это полезные предложения. Я изучу эти варианты и попытаюсь разработать пример кода. - person Stef van Buuren; 28.12.2017
comment
Если вы выберете подход cbind2/rbind2 (но, возможно, и для '...'), вам придется использовать setOldClass("mids") [или версию с большим количеством аргументов, например S4class = ., см. также ` ?Methods_for_S3 ` -- относительно недавний (2016 г.) BTW, и хорошая технология, возможно, еще не использовалась широко... и поэтому мы (я, JMC,..) можем быть заинтересованы в помощи. - person Martin Mächler; 29.12.2017

Подход dotsMethod был предложен Мартином Мейхлером и реализован в пакете Rmpfr. Нам нужно определить новый дженерик, класс и метод, используя S4.

setGeneric("rbind", signature = "...")
mychar <- setClass("myclass", slots = c(x = "character"))
b <- mychar(x = "b")
rbind.myclass <- function(...) "hello from rbind.myclass"
setMethod("rbind", "myclass",
      function(..., deparse.level = 1) {
        args <- list(...)
        if(all(vapply(args, is.atomic, NA)))
          return( base::cbind(..., deparse.level = deparse.level) )
        else
          return( rbind.myclass(..., deparse.level = deparse.level))
      })

# these work as expected
rbind(b, "d")
rbind(b, b)
rbind(b, matrix())

# this fails in R 3.4.3
rbind(b, data.frame())

Error in rbind2(..1, r) :
    no method for coercing this S4 class to a vector

Я не смог устранить ошибку. См. R: не следует универсальные методы работают внутри пакета без его прикрепления? для связанной проблемы.

Поскольку этот подход переопределяет rbind(), мы получаем предупреждение The following objects are masked from 'package:base': rbind.

person Stef van Buuren    schedule 28.12.2017
comment
Я думаю, что это будет работать только в том случае, если (1) myclass является объектом S4; (2) он не наследуется от data.frame; (3) вы устанавливаете ClassUnion(myclass, data.frame) на что-то, скажем, myclass_data.frame; (4) вы определяете rbind.myclass_data.frame. Я могу ошибаться здесь, но если нет, я все еще придерживаюсь своего ответа ниже. - person Patrick Perry; 28.12.2017

Я не думаю, что ты сможешь придумать что-то полностью удовлетворяющее. Лучшее, что вы можете сделать, это экспортировать rbind.myclass, чтобы пользователи могли вызывать его напрямую, не выполняя mypackage:::rbind.myclass. Вы можете назвать его как-то иначе, если хотите (dplyr называет свою версию bind_rows), но если вы решите это сделать, я бы использовал имя, которое вызывает ассоциации с rbind, например rbind_myclass.

Даже если вы можете заставить r-core согласиться изменить поведение отправки, чтобы rbind выполняла отправку по первому аргументу, все равно будут случаи, когда пользователи захотят rbind несколько объектов вместе с myclass объектом где-нибудь, кроме первого. . Как еще пользователи могут отправлять сообщения на rbind.myclass(df, df, myclass)?

Решение data.table кажется опасным; Я не удивлюсь, если сопровождающие CRAN в какой-то момент поставят галочку и запретят это.

person Patrick Perry    schedule 26.12.2017
comment
Согласен. Лучше всего экспортировать rbind.myclass() и вызывать rbind(). Дело в том, что вы получите неверный ответ, если первым аргументом будет myclass, а вторым или более поздним аргументом будет data.frame. Переименование позволяет избежать этой проблемы, но недостатком является то, что пользователю придется иметь дело с несколькими именами для концептуально одной и той же операции. В R уже есть слишком много примеров этого. Кроме того, это сломало бы мой существующий код. Вызов rbind(df, df, myclass) отправляется на base::rbind.data.frame(), что правильно. rbind() обрабатывает несколько аргументов по задумке, поэтому нет необходимости это менять. - person Stef van Buuren; 27.12.2017