Тем, кто работает с R, хорошо известно, что изначально язык разрабатывался как инструмент для интерактивной работы. Естественно, что методы удобные для консольного пошагового применения человеком, который глубоко в теме, оказываются малопригодными для создания приложения для конечного пользователя. Возможность получить развернутую диагностику сразу по факту ошибки, проглядеть все переменные и трейсы, выполнить вручную элементы кода (возможно, частично изменив переменные) — все это будет недоступно при автономной работе R приложения в enterprise среде. (говорим R, подразумеваем, в основном, Shiny web приложения).
Однако, не все так плохо. Среда R (пакеты и подходы) настолько сильно эволюционировали, что ряд весьма нехитрых трюков позволяет элегантно решать задачу обеспечения стабильности и надежности работы пользовательских приложений. Ряд из них будет описан ниже.
Является продолжением предыдущих публикаций.
В чем сложность задачи?
Основной спектр задач для которых часто применяется R — разнообразная обработка данных. И даже полностью отлаженный алгоритм, обложеные со всех сторон тестами и полностью задокументированный может легко сломаться и выдать ерунду, если ему на вход подсунут кривые данные.
Данные могут поступать на вход как от других информационных систем, так и от пользователей. И, если в первом случае можно требовать соблюдения API и накладывать весьма жесткие ограничения на стабильность информационного потока, то во втором случае от сюрпризов никуда не деться. Человек может ошибиться и подсунуть не тот файл, написать в него не то. 99% пользователей используют в своей работе Excel и предпочитают подсовывать системе именно его, много страничный, с хитрым форматированием. В этом случае задача еще больше усложняется. Даже визуально валидный документ может выглядеть с точки зрения машины полной ерундой. Даты разъезжаются (весьма известная история «Excel’s designer thought 1900 was a leap year, but it was not»). Числовые значения хранятся как текст и наборот. Невидимые ячейки и скрытые формулы… И многое другое. Предусмотреть все возможные грабли в принципе не получится — фантазии не хватит. Чего стоит только задвоение записей в различных join-ах с кривыми источниками.
В качестве дополнительных соображенией примем следующие:
Прекрасный документ «An introduction to data cleaning with R», описывающий процесс предварительной подготовки данных. Для дальнейших шагов из него мы выделим наличие двух фаз валидации: техническая и логическая.
- Техническая валидация заключается в проверке корректности источника данных. Структура, типы, количественные показатели.
- Логическая валидация может быть многоэтапной, осуществляемой по ходу проведения расчетов, и заключается в проверке соответствия тех или иных элементов данных или их комбинаций различным логическим требованиям.
- Одно из базовых правил при разработке пользовательских интерфейсов — формирование максимально полной диагностики в случае ошибок пользователей. Т.е., если уж пользователь загрузил файл, то надо его максимально проверить на корректность и выдать полную сводку со всеми ошибками (желательно еще и объяснить, что где не так), а не падать при первой же проблеме с сообщением вида «Incorrect input value @ line 528493, pos 17» и требовать загрузки нового файла с исправленной этой ошибкой. Такой подход позволяет многократно сократить количество итераций по формированию правильного источника и повысить качество конечного результата.
Технологии и методы валидации
Пойдем с конца. Для логической валидации существует ряд пакетов. В нашей практике мы остановились на следующих подходах.
- Уже классический
dplyr
. В простых случаях бывает удобно просто нарисовать pipe c проведением ряда проверок и анализом конечного результата. - Пакет
validate
для проверки технически корректных объектов на соответствие заданным правилам.
Для технической валидации остановились на следующих подходах:
- Пакет
checkmate
с широким спектром быстрых функций для проведения разнобразных технических проверок. - Явная работа с исключениями «Advanced R. Debugging, condition handling, and defensive programming», «Advanced R. Beyond Exception Handling: Conditions and Restarts» как для проведения полного объема валидации за один шаг, так и для обеспечения стабильности работы приложения.
- Использование
purr
обертки для исключений. Весьма полезно при применении внутри pipe.
В коде, разбитом на функции, важным элементом «defensive programming» является проверка входных и выходных параметров функций. В случае языков с динамической типизацией проверку типов приходится делать самостоятельно. Для базовых типов идеально подходит пакет checkmate, особенно его функции qtest
\qassert
. Для проверки data.frame
остановились на примерно следующей конструкции (проверка имен и типов). Трюк со слиянием имени и типа позволяет сократить количество строк в проверке.
ff <- function(dataframe1, dataframe2){
# достали имя текущей функции для задач логирования
calledFun <- deparse(as.list(sys.call())[[1]])
tic("Calculating XYZ")
# проверяем содержимое всех входных дата фреймов (class, а не typeof, чтобы Date отловить)
list(dataframe1=c("name :: character", "val :: numeric", "ship_date :: Date"),
dataframe2=c("out :: character", "label :: character")) %>%
purrr::iwalk(~{
flog.info(glue::glue("Function {calledFun}: checking '{.y}' parameter with expected structure '{collapse(.x, sep=', ')}'"))
rlang::eval_bare(rlang::sym(.y)) %>%
assertDataFrame(min.rows=1, min.cols=length(.x)) %>%
{assertSetEqual(.x, stri_join(names(.), map_chr(., class), sep=" :: "), .var.name=.y)}
# {assertSubset(.x, stri_join(names(.), map_chr(., typeof), sep=" :: "))}
})
…
}
В части функции проверки типов можно выбирать метод по вкусу, сообразуясь с ожидаемыми данными. class
был выбран, поскольку именно он дает дату как Date
, а не как число (внутреннее представление). Очень подробно вопрос определения типов данных разбирается в диалоге «A comprehensive survey of the types of things in R. 'mode' and 'class' and 'typeof' are insufficient».
assertSetEqual
или assertSubset
выбираются из соображений четкого совпадения колонок или же минимально достаточного.
Для практических задач такой небольшой набор вполне покрывает большую часть потребностей.
Предыдущая публикация — R как спасательный круг для системного администратора.
Комментарии (6)
i_shutov Автор
12.06.2018 13:08да нет, скорее исторически сложилось, когда смотрели на существующие пакеты некоторое время назад. Структурный же анализ делаем посредством
checkmate
.
Логические проверки бывают в целом несложные (диапазоны, зависимости). Удобно скидывать правила в файлы или генерировать файлы с правилами внешними скриптами или внешними админскими интерфейсами. Да и когда всегда есть
dplyr
под рукой, с помощью которого можно выполнить любую сложную валидацию, особого пристрастия высказывать не стали. Свою задачу решает, в целом удобно, ну и замечательно. Появится что лучше — перейдем. Ровно так и произошел год назад переход сassertr
наcheckmate
.
Может у Вас есть кейс, где
ruler
однозначно "рулит"? Интересно было бы ознакомиться и взять на заметку.
WinDigo
12.06.2018 13:54Спасибо за развёрнутый ответ.
Относительно кейса у меня было всё достаточно стандартно. Есть data frame, на строки и ячейки которого накладываются ряд ограничений, очень удобно реализуемые с помощью
dplyr
. Необходимо написать функцию, которая проверяет выполнение всех ограничений. При выявлении "плохих" элементов необходимо подать какой-то сигнал (ошибку, предупреждение, сообщение) и вывести в консоль отчёт в компактном виде.
Для данной задачи
ruler
подошёл практически идеально. Конечно, рассматривал иvalidate
, иassertr
. Первый на тот момент мне показался немного переусложнённым и не позволял получить необходимый отчёт без особых танцев с бубном. Второй выполнял код очень медленно, потому что вызывал функции проверки чуть ли не построчно (вместо векторизованного варианта при создании правил черезdplyr
)
dimastrow
13.06.2018 13:37Трюк со слиянием имени и типа позволяет сократить количество строк в проверке — весьма полезно :) Как всегда отличная статья, продолжайте.
WinDigo
Для достаточно удобного совмещения описанных подходов логической валидации могу порекомендовать пакет ruler. Позволяет автоматизировать процесс «анализа конечного результата» после создания «pipe c проведением ряда проверок». В нём правила валидации определяются непосредственно в виде pipe функциональной последовательности, а результаты проверки сохраняются в виде data frame фиксированного формата.
i_shutov Автор
между
validate
иruler
остановились на 1-м, но это не характеризует качество пакетов, а просто персональные предпочтения в использовании конструкций R и метапрограммированияWinDigo
А можно немного поподробнее, пожалуйста? Вы имеете в виду формат результатов валидации (объект S4 против tibble) или что-то другое?