Насколько R быстр для продуктива?

в 7:33, , рубрики: big data, data mining, data science, R

Есть такой популярный класс задач, в которых требуется проводить достаточно глубокий анализ всего объема цепочек работ, регистрируемых какой-либо информационной системой (ИС). В качестве ИС может быть документооборот, сервис деск, багтрекер, электронный журнал, складской учет и пр. Нюансы проявляются в моделях данных, API, объемах данных и иных аспектах, но принципы решения таких задач примерно одинаковы. И грабли, на которые можно наступить, тоже во многом похожи.

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

Является продолжением предыдущих публикаций.

Обычно, поверхностный подход «в лоб» не является самым эффективным. 99% задач, связанных с анализом и обработкой данных начинаются с их импорта. В этом кратком очерке рассмотрим проблемы, возникающие на базовом этапе импорта данных, на примере типовой задачи «глубокого» анализа данных инсталляции Jira.

Постановка задачи

Дано:

  • jira внедрена и используется в процессе разработки ПО как система управления задачами и багтрекер.
  • Прямого доступа к БД jira нет, взаимодействие осуществляется через REST API (гальваническая развязка).
  • Забираемые json файлы имеют весьма сложную древовидную структуру с вложенными кортежами, требуемые для выгрузки всей истории действий. Для расчета же метрик требуется относительно небольшое количество параметров, разбросанных по разным уровням иерархии.

Пример штатного jira json на рисунке.

Насколько R быстр для продуктива? - 1

Требуется:

  • На основании данных jira необходимо найти узкие места и точки возможного роста эффективности процессов разработки и повышения качества получаемого продукта на основе анализа всех зарегистрированных действий.

Решение

Теоретически в R есть несколько различных пакетов по загрузке json и преобразованию их в data.frame. Наиболее удобным выглядит пакет jsonlite. Однако, прямое преобразование иерархии json в data.frame затруднительно в силу многоуровневого вложения и сильной параметризированности структуры записей. Выцепление конкретных параметров, связанных, например, с историей действий, может потребовать различных доп. проверок и циклов. Т.е. задачу можно решить, но для json файла размером в 32 задачи (включает все артефакты и всю историю по задачам) такой нелинейный разбор средствами jsonlite и tidyverse занимает ~10 секунд на ноутбуке средней производительности.

Сами по себе 10 секунд — это немного. Но ровно до момента, пока этих файлов не становится слишком много. Оценка на сэмпле разбора и загрузки подобным «прямым» методом ~4000 файлов (~4 Гб) дала 8-9 часов работы.

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

Даже 10-15 итераций на этапе анализа данных, выявления необходимого минимального набора параметров, обнаружения исключительных или ошибочных ситуаций и выработки алгоритмов постпроцессинга дают затраты в размере 2-3 недели (только счетное время).
Естественно, что подобная «производительность» не подходит для операционной аналитики, встроенной в продуктивный контур, и очень неффективно на этапе первичного анализа данных и разработки прототипа.

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

Результирующее решение сводится к следующим 10 строчками (это сутевой скелет, без последующего нефункционального обвеса):

library(tidyverse)
library(jsonlite)
library(readtext)

fnames <- fs::dir_ls(here::here("input_data"), glob = "*.txt")

ff <- function(fname){
  json_vec <- readtext(fname, text_field = "texts", encoding = "UTF-8") %>%  
    .$text %>%
    jqr::jq('[. | {issues: .issues}[] | .[]', 
            '{id: .id, key: .key, created: .fields.created, 
             type: .fields.issuetype.name, summary: .fields.summary, 
             descr: .fields.description}]')
  jsonlite::fromJSON(json_vec, flatten = TRUE)
}
tictoc::tic("Loading with jqr-jsonlite single-threaded technique")
issues_df <- fnames %>% 
  purrr::map(ff) %>%
  data.table::rbindlist(use.names = FALSE)
tictoc::toc()

system.time({fst::write_fst(issues_df, here::here("data", "issues.fst"))})

Что здесь интересного?

  1. Для ускорения процесса загрузки хорошо использовать специализированные профилированные пакеты, такие как readtext.
  2. Применение потокового парсера jq позволяет перевести все выцепление нужных атрибутов на функциональный язык, опустить его на CPP уровень и минимизировать ручные манипуляции над вложенными списками или списками в data.frame.
  3. Появился очень перспективный пакет bench для микробенчамарков. Он позволяет изучать не только время исполнения операций, но и манипуляции с памятью. Не секрет, что на копировании данных в памяти можно терять очень много.
  4. Для больших объемов данных и простой обработки часто приходится в финальном решении отказываться от tidyverse и переводить трудоемкие части на data.table, в частности здесь идет слияние таблиц средствами именно data.table. А также все преобразования на этапе постпроцессинга (которые включены в цикл посредством функции ff также сделаны средствами data.table с подходом изменения данных по ссылке, либо пакетами, построенными с применением Rcpp, например, пакет anytime для работы с датами и временем.
  5. Для сброса данных в файл и последующего чтения очень хорош пакет fst. В частности, всего доли секунды уходят на сохранение всей аналитики jira истории за 4 года, а данные сохраняются именно как типы данных R, что хорошо для последующего их переиспользования.

В ходе решения был рассмотрен подход с применением пакета rjson. Вариант jsonlite::fromJSON примерно в 2 раза медленнее, чем rjson = rjson::fromJSON(json_vec), но пришлось оставить именно его, потому как в даных бывают NULL значения, а на этапе преобразования NULL в NA в списках, выдаваемых rjson мы теряем преимущество, а код утяжеляется.

Заключение

  1. Подобный рефакторинг привел к изменению времени процессинга всех json файлов в однопоточном режиме на этом же ноутбуке с 8-9 часов до 10 минут.
  2. Добавление параллелизации задачи средствами foreach практически не утяжелило код (+ 5 строчек) но снизило время исполнения до 5 минут.
  3. Перевод решения на слабенький linux сервер (всего 4 ядра), но работающего на SSD в многопоточном режиме свело время исполнения до 40 секунд.
  4. Публикация на продуктивный контур (20 ядер, 3 ГГц, SSD) дало снижение времени исполнения до 6-8 секунд, что является более чем приемлемым для задач операционной аналитики.

Итого, оставаясь в рамках платформы R, простым рефакторингом кода удалось добитьcя уменьшения времени исполнения с ~9 часов до ~9 секунд.

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

Предыдущая публикация — «Аналитический паRашют для менеджера».

Автор: i_shutov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js