Главная » Хабрахабр » Насколько R быстр для продуктива?

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

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

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

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

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

Дано:

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

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

Требуется:

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

Наиболее удобным выглядит пакет jsonlite. Теоретически в R есть несколько различных пакетов по загрузке json и преобразованию их в data.frame. Выцепление конкретных параметров, связанных, например, с историей действий, может потребовать различных доп. Однако, прямое преобразование иерархии 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)[] | .[]', '{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, что хорошо для последующего их переиспользования.

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


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

[Из песочницы] Контроллер, полегче! Выносим код в UIView

У вас большой UIViewController? У многих да. С одной стороны, в нём работа с данными, с другой — с интерфейсом. Они решают проблему потока данных, но не отвечают на вопрос как работать с интерфейсом: в одном месте остается создание элементов, лейаут, ...

Нужно больше разных Blur-ов

Размытие изображение посредством фильтра Gaussian Blur широко используется в самых разных задачах. Но иногда хочется чуть большего разнообразия, чем просто один фильтр на все случаи жизни, в котором регулировке поддаётся только один параметр — его размер. В этой статье мы ...