Обеспечение воспроизводимости в среде R - PullRequest
26 голосов
/ 04 ноября 2010

Я работаю в лаборатории вычислительной биологии, где у нас есть несколько человек, работающих над несколькими проектами, в основном на R (это то, что меня волнует в этом посте). В прошлом люди просто разрабатывали свой код для каждого проекта, что может включать или не включать стандартный код, скопированный из предыдущих проектов. Одна вещь, которую я настаивал на протяжении многих лет, заключалась в том, чтобы привнести некоторую централизованную структуру в этот беспорядок, и чтобы люди идентифицировали общие шаблоны, чтобы мы могли превратить эти повторяющиеся / общие блоки кода в пакеты по всем многим причинам, которые, как можно подумать, хорошая вещь, чтобы сделать. Так что теперь наши люди используют сочетание централизованных пакетов / подпрограмм в своих специфичных для проекта сценариях.

Здесь есть одна ошибка. У нас есть полномочия, заключающиеся в том, что каждый сценарий для каждого проекта должен воспроизводиться на 100% с течением времени в меру наших возможностей (и это включает в себя 100% всего кода, к которому мы имеем прямой доступ, включая наши пакеты). То есть, если я вызову функцию foo в панели пакетов с параметром A для получения результата X сегодня, через 4 года я должен получить точно такой же результат. (ошибочный вывод из-за ошибок здесь исключен)

Тема воспроизводимости возникала время от времени в R в различных кругах, но обычно она, кажется, обсуждается с точки зрения воспроизводимости процесса (например, виньетки). Это не одно и то же - я могу запустить виньетку сегодня, а через 6 месяцев запустить тот же код, используя обновленные пакеты, и получить совершенно разные результаты.

Решение, которое было согласовано (которым я не являюсь поклонником), заключается в том, что если необходимо изменить функцию или пакет в не обратно совместимом изменении, оно просто получает новое имя. Таким образом, если нам нужно радикально изменить функцию foo (), она будет называться foo2 (), а если для этого потребуется радикальное изменение, она получит название foo3 (). Это гарантирует, что любой скрипт, который вызвал foo (), всегда будет получать исходный результат, в то же время позволяя вещам продвигаться вперед в хранилище пакетов. Это работает, но мне очень не нравится это - это кажется эстетически чрезвычайно загроможденным, и я беспокоюсь, что со временем это приведет к массовой путанице с наличием пакетов bar, bar2, bar3, bar4 ... функций foo1, foo2, foo3 и т. Д.

Проблема в том, что я не нашел альтернативного решения, которое действительно лучше. Одна из возможностей заключается в том, чтобы записать номера версий пакетов, R и т. Д. И убедиться, что они загружены, но у этого есть несколько проблем - не в последнюю очередь из-за того, что он опирается на надлежащую дисциплину управления версиями пакетов, и это подвержено ошибкам. Кроме того, эта альтернатива уже была отвергнута;) В идеале у нас было бы какое-то представление о разработке и выпуске, поскольку большинство этих изменений, как правило, происходят раньше, а затем выравниваются с изменениями, происходящими гораздо реже. OTOH, что на самом деле означает здесь devel, это «еще не в пакете» (что мы и делаем), но может быть трудно точно определить, в какой момент правильно переносить вещи. В тот момент, когда вы думаете, что вы в безопасности, вы понимаете, что это не так.

Так что, учитывая все это, мне любопытно, сталкивался ли кто-нибудь еще с подобными ситуациями и как они могли бы разрешить проблемы.

edit: просто для ясности, из-за несовместимости с предыдущими версиями, я говорю не только об API и подобных, но и о выходных данных для заданного набора входных данных

Ответы [ 9 ]

20 голосов
/ 04 ноября 2010

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

  1. Контроль версий (svn, git, bzr, cvs и т. Д.)
  2. Модульные тесты

Моя первая реакция - вам нужно институционализировать какую-то систему управления кодом.Это облегчит задачу, потому что старая версия foo () все еще доступна, если вы действительно этого хотите.Из того, что вы сказали, звучит так, как будто вам нужно упаковать свои общие функции и установить какой-то график выпуска.Скрипты, которые требуют обратной совместимости, должны включать имя пакета и информацию о выпуске.Таким образом, ВСЕГДА можно получить foo () точно так, как это было при написании скрипта.Вы также должны убедиться, что люди используют только официальные версии релизов в своей работе, потому что в противном случае это может стать довольно болезненным.

Я согласен, имея коллекцию foo: foo99 обречена на провал.Но по крайней мере это будет великолепно сбивающий с толку провал.Помимо эстетики, это сведет вас с ума.Если foo2 () является улучшением (более точным, быстрым и т. Д.) Функции foo (), то она должна называться foo () и выпущена для использования в соответствии с графиком выпуска вашей компании.Если он делает что-то другое, он больше не является foo ().Это может быть fooo () или superFoo () или fooMe (), но это не foo ().

Наконец, вам нужно начать тестировать свои функции.(Модульные тесты) Для каждой функции, которая публикуется и предоставляется другим, у вас должен быть четко определенный набор тестов.Если кто-то не исправит ошибку в foo (), результаты должны остаться прежними.Если кто-то исправляет ошибку, то результаты должны быть более точными и, вероятно, в большинстве случаев более желательными.Если вам нужно воспроизвести старые, неправильные результаты, вы можете извлечь старую версию foo () из вашей системы контроля версий.Проводя строгие модульные тесты, вы будете знать, изменились ли / когда результаты foo.Эти знания должны помочь минимизировать количество необходимых вам функций foo ().Вместо того чтобы создавать версию каждый раз, когда кто-то что-то настраивает, вы можете протестировать новую версию, чтобы увидеть, соответствуют ли результаты ожиданиям.Но это сложно, потому что вы должны убедиться, что ваши тесты охватывают все, что функция когда-либо может увидеть, включая причудливые крайние случаи.В исследовательской обстановке я мог бы представить, что это может стать проблемой.

8 голосов
/ 04 ноября 2010

Я не уверен насчет интеграции его с R, но Суматра может стоить изучить.Похоже, чтобы вы могли отслеживать код и результаты.Так что, если вам нужно вернуться к этой симуляции 4 года назад, код должен быть там.

5 голосов
/ 23 июня 2011

Всякий раз, когда вы хотите заморозить свой код так, чтобы его можно было воспроизвести «навсегда», например, когда ваша статья была опубликована, самый безопасный способ сделать это - создать виртуальную машину, содержащую весь ваш код и данные, ипрограммное обеспечение, необходимое для его запуска (включая операционную систему).Вот пример на сайте Университета Вашингтона .

5 голосов
/ 04 ноября 2010

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

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

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

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

x <- read.table("sometable")
y <- ColSums(x)/4.3

и настройку значений, или набрав

myfun <- function(i,j){
  x <- read.table(i)
  y <- ColSums(x)/j
}

Спасает вас и многих других людей от множества проблем с копированием. (Как так, объект не найден? Какой объект?)

3 голосов
/ 04 ноября 2010

Что если изменение в результате связано с изменением вашей операционной системы?Возможно, Microsoft исправит ошибку в Windows XP для Windows 7, а затем при обновлении - все ваши результаты будут разными.

Если вы хотите справиться с этим, то я думаю, что лучший способ работы - это сохранять снимки виртуальных машин.когда вы закрываете анализ и сохраняете образы виртуальных машин для дальнейшего использования.Конечно, через пять лет у вас не будет лицензии на запуск Windows XP, так что это еще одна проблема, которая решается с помощью операционной системы с открытым исходным кодом, такой как Linux.

3 голосов
/ 04 ноября 2010

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

Такое мышление означает, что все ошибки остаются ошибками для обеспечения согласованности.Это мышление перенесено из корпоративной бюрократии и не имеет никакого дела в научной лаборатории.

Единственный способ сделать это - сохранить копию всех ваших пакетов и версию R вместе с вашим кодом.Центральная корпорация не обязана проверять совместимость, которая позаботится об этом за вас.

2 голосов
/ 19 декабря 2015

Я бы пошел с образами докера.
Это довольно удобный способ воспроизвести ОС и все зависимости.
Вы создаете образ, а затем можете в любое время развернуть его в докере, он будет полностью настроен.
Вы можете найти несколько доступных изображений докеров R, чтобы вы могли легко создать на них свой образ.
Уже построив образ, вы можете использовать его для развертывания в тестовой среде, а затем в Production.

1 голос
/ 12 марта 2014

Решение может заключаться в использовании S4 методов и разрешении работы внутреннего диспетчера R за вас (см. Пример ниже). Таким образом, вы несколько «пуленепробиваемы» в том, что можете систематически обновлять ваш код, не рискуя что-то сломать.

Основные преимущества

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

Таким образом, ваша функция всегда будет foo (в отличие от необходимости отслеживать foo1, foo2 и т. Д.), В то время как новые функциональные возможности могут быть легко реализованы (путем добавления соответствующих методов), не касаясь «старых» методы (на которые могут полагаться другие люди / пакеты).

Основные функции, которые вам понадобятся:

  • setGeneric
  • setMethod
  • setRefClass (справочные классы S4; личная рекомендация) или setClass (класс S4; я не буду использовать их по причине, описанной в «Дополнительные замечания» в самом конце)

"Минусы"

  • Вам необходимо переключиться с логики S3 на S4

  • Это означает, что вам нужно написать немного больше кода, чем то, к чему вы могли бы привыкнуть (общие определения методов, определения методов и, возможно, собственные определения классов (см. Пример ниже). Но это «покупает» вас и ваш код намного более структурирован и делает его более устойчивым.

  • Это может также подразумевать, что вы в конечном итоге будете копать все глубже и глубже в мир объектно-ориентированного программирования или объектно-ориентированного проектирования, Хотя я лично считаю, что это хорошо (мое личное эмпирическое правило: чем сложнее / распределено ваше приложение, тем лучше вы отказываетесь от использования ООП), некоторые считают эти подходы R-нетипичными (я категорически не согласен с R имеет превосходные OO-функции, которые поддерживаются Core Team ) или "не подходит" для R (это может быть правдой в зависимости от того, насколько вы полагаетесь на "не-OOP") пакеты / код). Если вы хотите пойти по этому пути, возможно, вы захотите ознакомиться с SOLID принципами объектно-ориентированного проектирования. Вы также можете проверить следующие книги: Чистый кодер и Прагматичный программист .

  • Если эффективность вычислений (например, при оценке статистических моделей) действительно важна, использование методов S4 и эталонных классов S4 может немного замедлить работу. В конце концов, здесь больше кода по сравнению с S3. Но я бы рекомендовал проверить влияние этого от случая к случаю через system.time() и / или microbenchmark::microbenchmark() вместо выбора «идеологических» сторон (S3 против S4).


Пример

Начальная функция

Предположим, вы находитесь в отделе A , и кто-то в вашей команде начал с создания функции с именем foo()

foo <- function(x, y) {
    x + y
}
foo(x=10, y=20)

Первый запрос на изменение

Вы хотели бы иметь возможность расширять его, не нарушая "старый" код, основанный на foo().

Теперь, я думаю, мы все согласны с тем, что это может быть довольно сложно сделать.

Вам либо нужно явно изменить исходный код foo() (каждый раз, когда существует риск того, что вы сломаете что-то, что уже использовалось для работы; это нарушает "O" в SOLID : Откройте Closed-Principle ) или вам нужно указать альтернативные имена, такие как foo1, foo2 и т. Д. (Очень сложно отследить, какая функция выполняет что-либо).

foo <- function(x, y, type=c("old", "new")) {
    type <- match.arg(type, choices=c("old", "new")) 
    if (type == "old") {
        x + y
    } else if (type == "new") {
        x * y    
    }
}
foo(x=10, y=20)
[1] 30
foo(x=10, y=20, type="new")
[1] 200

foo1 <- function(x, y) {
    x * y
}
foo1(x=10, y=20)
[1] 200

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

Общий метод

Вам нужно начать с превращения foo() в универсальный метод.

setGeneric(
    name="foo",
    signature=c("x", "y", ".ctx", ".ns"),
    def=function(x, y, ..., .ctx, .ns) {
        standardGeneric("foo")
    }
)

Проще говоря: сам универсальный метод еще ничего не делает. Это просто предварительное условие для того, чтобы иметь возможность указывать «фактические» методы для своих аргументов подписи, которые делают что-то полезное.

Аргументы подписи

Степень гибкости по отношению к исходной проблеме напрямую связана с числом аргументов подписи, которые вы объявляете (signature=c("x", "y", ".ctx", ".ns")): чем больше аргументов подписи, тем больше гибкости у вас есть, но чем сложнее может быть код хорошо (относительно того, сколько кода вы должны написать).

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

Представьте себе, что диспетчер методов похож на клерка в бизнесе проката лыж: вы даете ему произвольный большой набор информации о подписи (то есть информации, которая «четко отличает вас от других»: ваш возраст, рост размер обуви и уровень квалификации), и он использует эту информацию, чтобы предоставить вам необходимое снаряжение для катания по склонам. Думайте о диспетчере методов R как о клерке, у которого есть доступ к камере хранения лыжного проката. Но вместо лыжного снаряжения он вернет методы.

Обратите внимание, что мы сказали, что наши "старые" аргументы x и y отныне должны быть аргументами подписи, а также есть два новых аргумента: .ctx и .ns. Я доберусь до них через минуту. Именно эти аргументы обеспечат нам гибкость, к которой мы стремимся.

Начальное определение метода

Теперь мы определим «вариант» (метод) универсального метода для следующего «сценария подписи»:

  1. x является numeric
  2. y is numeric
  3. .ctx просто не будет предоставляться при вызове метода и поэтому будет missing
  4. .ns просто не будет предоставляться при вызове метода и поэтому будет missing

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

setMethod(
    f="foo", 
    signature=signature(x="numeric", y="numeric", .ctx="missing", .ns="missing"), 
    definition=function(x, y, ..., .ctx, .ns) {
        x + y
    }
)

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

foo(x=10, y=20)
[1] 30

Первое обновление

Теперь кто-то из отдела B приходит, смотрит на foo(), любит его, но решает, что foo() необходимо обновить (x * y вместо x + y), если он должен быть используется в его отделе.

Именно тогда в игру вступает .ctx (сокращение от context ): это аргумент, по которому мы можем различить контексты приложения .

Определение класса, который представляет новый контекст приложения

setRefClass("ApplicationContextDepartmentB")

При вызове foo() мы предоставим ему экземпляр этого класса (.ctx=new("ApplicationContextDepartmentB"))

Определение нового метода для нового контекста приложения

Обратите внимание, как мы регистрируем аргумент подписи .ctx в нашем новом классе ApplicationContextDepartmentB:

setMethod(
    f="foo", 
    signature=signature(x="numeric", y="numeric", 
        .ctx="ApplicationContextDepartmentB", .ns="missing"), 
    definition=function(x, y, ..., .ctx, .ns) {
        out <- x * y
        attributes(out)$description <- "I'm different from the original foo()"
        return(out)
    }
)

Таким образом, диспетчер методов точно знает, что он должен возвращать «новый» метод вместо «старого» метода, когда мы вызываем foo() следующим образом:

foo(x=1, y=10, .ctx=new("ApplicationContextDepartmentB"))
[1] 10
attr(,"description")
[1] "I'm different from the original foo()"

На "старый" метод это никак не влияет:

foo(x=1, y=10)
[1] 30

Второе обновление

Предположим, что кто-то из отдела C приходит и предлагает еще одну "конфигурацию" или версию для foo(). Вы можете легко это сделать, не нарушая ничего, что вы реализовали для отделов A и B до настоящего момента, выполнив ту же процедуру, что и для отдела B .

Но мы даже сделаем еще один шаг: мы определим два дополнительных класса, которые позволят нам различать разные «пространства имен» (вот где .ns вступает в игру).

Думайте о пространствах имен как о способе различения различных сценариев времени выполнения для конкретного метода для конкретного контекста приложения (т. Е. «Тестирование» и «продуктивный режим»).

Определение классов

setRefClass("ApplicationContextDepartmentC")
setRefClass("TestNamespace")
setRefClass("ProductionNamespace")

Определение нового метода для нового контекста приложения и «тестового» сценария

Обратите внимание, как мы регистрируем аргументы подписи .ctx для нашего нового класса ApplicationContextDepartmentC и .ns для нашего нового класса TestNamespace:

setMethod(
    f="foo", 
    signature=signature(x="character", y="numeric", 
        .ctx="ApplicationContextDepartmentC", .ns="TestNamespace"), 
    definition=function(x, y, ..., .ctx, .ns) {
        data.frame(x, y, test.ok=rep(TRUE, length(x)))
    }
)

Опять же, диспетчер методов будет искать правильный метод при вызове foo() следующим образом:

foo(x=letters[1:5], y=11:15, .ctx=new("ApplicationContextDepartmentC"), 
    .ns=new("TestNamespace"))
  x  y test.ok
1 a 11    TRUE
2 b 12    TRUE
3 c 13    TRUE
4 d 14    TRUE
5 e 15    TRUE

Определение нового метода для нового контекста приложения и «производительного» сценария

setMethod(
    f="foo", 
    signature=signature(x="character", y="numeric", 
        .ctx="ApplicationContextDepartmentC", .ns="ProductionNamespace"), 
    definition=function(x, y, ..., .ctx, .ns) {
        data.frame(x, y)
    }
)

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

foo(x=letters[1:5], y=11:15, .ctx=new("ApplicationContextDepartmentC"), 
    .ns=new("ProductionNamespace"))

  x  y
1 a 11
2 b 12
3 c 13
4 d 14
5 e 15

Обратите внимание, что вы можете использовать классы TestNamespace и ProductionNamespace где угодно. Эти классы никак не связаны с ApplicationContextDepartmentC, так что вы можете, например, также использовать для всех других ваших сценариев применения.

Дополнительные замечания к определениям методов

Часто полезно использовать метод, который принимает классы ANY для аргументов сигнатуры и определяет более строгие методы по мере развития вашего программного обеспечения:

setMethod(
    f="foo", 
    signature=signature(x="ANY", y="ANY", .ctx="missing", .ns="missing"), 
    definition=function(x, y, ..., .ctx, .ns) {
        message("Value of x:")
        print(x)
        message("Value of y:")
        print(y)
    }
)
foo(x="Hello World!", y=rep(TRUE, 3))
Value of x:
[1] "Hello World!"
Value of y:
[1] TRUE TRUE TRUE

Дополнительные замечания к определениям классов

Я предпочитаю ссылочные классы S4, а не классы S4 из-за возможностей самоссылки эталонных классов S4:

setRefClass(
    Class="A", 
    fields=list(
        x1="numeric",
        x2="logical"
    ),
    methods=list(
        getX1=function() {
            .self$x1
        },
        getX2=function() {
            .self$x2
        },
        setX1=function(x) {
            .self$x1 <- x
        },
        setX2=function(x) {
            .self$field("x2", x)
        },
        addX1AndX2=function() {
            .self$getX1() + .self$getX2()
        }
    )
)
x <- new("A", x1=10, x2=TRUE)
x$getX1()
[1] 10
x$getX2()
[1] TRUE
x$addX1AndX2()
[1] 11

S4 Классы не имеют этой функции.

Последующие модификации значений полей:

x$setX1(100)
x$addX1AndX2()
[1] 101
x$x1 <- 1000
x$addX1AndX2()
[1] 1001

Дополнительные замечания по документированию методов и классов

Я настоятельно рекомендую использовать пакеты roxygen2 и devtools для документирования ваших методов и классов. Возможно, вы также захотите посмотреть на пакет roxygen3.

Документирование общих методов с помощью roxygen2:

#' Foo
#'
#' This method takes \code{x} and \code{y} and adds them.
#' 
#' Some details here
#' 
#' @param x \strong{Signature argument}.
#' @param y \strong{Signature argument}.
#' @param ... Further arguments to be passed to subsequent functions.
#' @param .ctx \strong{Signature argument}.
#'      Application context.
#' @param .ns \strong{Signature argument}.
#'      Application namespace. Usually used to distinguish different context 
#'      versions or configurations.
#' @author Janko Thyson \email{john.doe@@something.com}
#' @references \url{http://www.something.com/}
#' @example inst/examples/foo.R
#' @docType methods
#' @rdname foo-methods
#' @export

setGeneric(
    name="foo",
    signature=c("x", "y", ".ctx", ".ns"),
    def=function(x, y, ..., .ctx, .ns) {
        standardGeneric("foo")
    }
)

Методы документирования с помощью roxygen2:

#' @param x \code{\link{character}}. Character vector.
#' @param y \code{\link{numeric}}. Numerical vector.  
#' @param .ctx \code{\link{ApplicationContextDepartmentC}}. 
#' @param .ns \code{\link{ProductionNamespace}}.  
#' @return \code{\link{data.frame}}. Some data frame.
#' @rdname foo-methods
#' @aliases foo,character,numeric,missing,missing-method
#' @export

setMethod(
    f="foo", 
    signature=signature(x="character", y="numeric", 
        .ctx="ApplicationContextDepartmentC", .ns="ProductionNamespace"), 
    definition=function(x, y, ..., .ctx, .ns) {
        data.frame(x, y)
    }
)
1 голос
/ 02 августа 2011

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

myFunction <- function(..., version = "latest"){
  if((version == "latest") || (version == 6)){
    return(myFunction06(...))
  } ...
  if((version == 1)){
    return(myFunction01(...))
  }
 }

Тогда код должен просто указать, какую версию он хочет. Как только фактическая функция стабилизируется, я прекращаю поддержку более старых версий функции, и быстрый поиск в моем коде позволяет мне находить любые оскорбительные вызовы. Использование «последней» означает, что я могу заверить, что вызывающая сторона и функция соответствуют некоторым довольно фиксированным определениям.

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

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