Тем, кто работает с 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-ах с кривыми источниками.


В качестве дополнительных соображенией примем следующие:


  1. Прекрасный документ «An introduction to data cleaning with R», описывающий процесс предварительной подготовки данных. Для дальнейших шагов из него мы выделим наличие двух фаз валидации: техническая и логическая.


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

  2. Одно из базовых правил при разработке пользовательских интерфейсов — формирование максимально полной диагностики в случае ошибок пользователей. Т.е., если уж пользователь загрузил файл, то надо его максимально проверить на корректность и выдать полную сводку со всеми ошибками (желательно еще и объяснить, что где не так), а не падать при первой же проблеме с сообщением вида «Incorrect input value @ line 528493, pos 17» и требовать загрузки нового файла с исправленной этой ошибкой. Такой подход позволяет многократно сократить количество итераций по формированию правильного источника и повысить качество конечного результата.

Технологии и методы валидации


Пойдем с конца. Для логической валидации существует ряд пакетов. В нашей практике мы остановились на следующих подходах.


  1. Уже классический dplyr. В простых случаях бывает удобно просто нарисовать pipe c проведением ряда проверок и анализом конечного результата.
  2. Пакет validate для проверки технически корректных объектов на соответствие заданным правилам.

Для технической валидации остановились на следующих подходах:


  1. Пакет checkmate с широким спектром быстрых функций для проведения разнобразных технических проверок.
  2. Явная работа с исключениями «Advanced R. Debugging, condition handling, and defensive programming», «Advanced R. Beyond Exception Handling: Conditions and Restarts» как для проведения полного объема валидации за один шаг, так и для обеспечения стабильности работы приложения.
  3. Использование 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)


  1. WinDigo
    12.06.2018 12:10

    Для достаточно удобного совмещения описанных подходов логической валидации могу порекомендовать пакет ruler. Позволяет автоматизировать процесс «анализа конечного результата» после создания «pipe c проведением ряда проверок». В нём правила валидации определяются непосредственно в виде pipe функциональной последовательности, а результаты проверки сохраняются в виде data frame фиксированного формата.


    1. i_shutov Автор
      12.06.2018 12:13

      между validate и ruler остановились на 1-м, но это не характеризует качество пакетов, а просто персональные предпочтения в использовании конструкций R и метапрограммирования


      1. WinDigo
        12.06.2018 12:40

        А можно немного поподробнее, пожалуйста? Вы имеете в виду формат результатов валидации (объект S4 против tibble) или что-то другое?


  1. i_shutov Автор
    12.06.2018 13:08

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


    Логические проверки бывают в целом несложные (диапазоны, зависимости). Удобно скидывать правила в файлы или генерировать файлы с правилами внешними скриптами или внешними админскими интерфейсами. Да и когда всегда есть dplyr под рукой, с помощью которого можно выполнить любую сложную валидацию, особого пристрастия высказывать не стали. Свою задачу решает, в целом удобно, ну и замечательно. Появится что лучше — перейдем. Ровно так и произошел год назад переход с assertrна checkmate.


    Может у Вас есть кейс, где ruler однозначно "рулит"? Интересно было бы ознакомиться и взять на заметку.


  1. WinDigo
    12.06.2018 13:54

    Спасибо за развёрнутый ответ.


    Относительно кейса у меня было всё достаточно стандартно. Есть data frame, на строки и ячейки которого накладываются ряд ограничений, очень удобно реализуемые с помощью dplyr. Необходимо написать функцию, которая проверяет выполнение всех ограничений. При выявлении "плохих" элементов необходимо подать какой-то сигнал (ошибку, предупреждение, сообщение) и вывести в консоль отчёт в компактном виде.


    Для данной задачи ruler подошёл практически идеально. Конечно, рассматривал и validate, и assertr. Первый на тот момент мне показался немного переусложнённым и не позволял получить необходимый отчёт без особых танцев с бубном. Второй выполнял код очень медленно, потому что вызывал функции проверки чуть ли не построчно (вместо векторизованного варианта при создании правил через dplyr)


  1. dimastrow
    13.06.2018 13:37

    Трюк со слиянием имени и типа позволяет сократить количество строк в проверке — весьма полезно :) Как всегда отличная статья, продолжайте.