Тем, кто работает с 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 как спасательный круг для системного администратора.