- PVSM.RU - https://www.pvsm.ru -
Довольно часто enterprise задачи по обработке данных затрагивают данные, сопровождаемые временной меткой. В R такие метки, обычно хранятся как класс POSIXct
. Выбор методов работы с таким типом данных по принципу аналогии может привести к большому разочарованию и убеждению о крайней медлительности R. Хотя если взглянуть на эту чуть более пристально, то оказывается, что дело не совсем в R, а в руках и голове.
Ниже затрону пару кейсов, которые встретились в этом месяце и возможные варианты их решения. В ходе решения появляются весьма интересные вопросы. Заодно упомяну инструменты, которые оказываются крайне полезными для решения подобных задачек. Практика показала, что об их существовании знают немногие.
С точки зрения бизнеса задача «как бы» тривиальная. Необходимо в доходчивом виде отобразить событийный поток распределенной системы за определённый временной промежуток. А потом обеспечить разнообразную критериальную «нарезку». В качестве источника данных выступают подсистемы логирования компонентов ИТ системы.
С позиции удобства восприятия для высокоуровневого обзора подобных данных оптимально подходит представление в виде «тепловой карты». Вот пример сгенерированного графика.
Изобретать велосипед смысла нет, был использован подход, описанный в статье «Bob Rudis, Making Faceted Heatmaps with ggplot2» [1]. Собственно говоря, поскольку данные по своей сути идентичны и представляют собой временную метку с соответствующим набором метрик, далее я буду апеллировать к этой статье и публично опубликованным данным, дабы не раскрывать частную информацию.
заключается в том, что источников поступления событий несколько и находятся они в разных часовых поясах. Время в исходных данных измеряется в UTC, а аналитику надо привязывать к рабочему циклу по времени локального часового пояса. Исходные данные выглядят следующим образом:
где timestamp
— UTC время события в формате ISO8601 в текстовом виде, tz — маркер тайм-зоны в которой это событие произошло.
заключается в том, что используемый в статье подход dplyr::do()
является устаревшим с момента выхода пакета purrr
. Комментарий автора: dplyr::do() is now basically deprecated in favour of the purrr approach [2]. А это значит, что код надо бы адаптировать под современный подход.
Сразу переходим к purrr
, пропускаем подходы, связанные с циклами for и враппером Vectorize
, как неэффективные. К сожалению, попытки применить «в лоб» векторизацию к POSIXct
векторам значений с различным timezone невозможно. Ответ находится в результате пристального взгляда на вектор POSIXct
значений функцией dput
. Timezone является атрибутом вектора, но не отдельного элемента!
По этой причине применение функционального подхода map
к POSIXct
переменной методом поэлементной обработки дает удручающий результат: на выборке в 20 тыс записей время процессинга составляет почти 20 секунд. При этом используются свойства векторизации функции ymd_hms()
, в противном случае результат был бы ещё хуже.
attacks_raw <- read_csv("./data/eventlog.csv", col_types="ccc", progress=interactive()) %>%
slice(1:20000)
attacks <- attacks_raw %>%
# mutate(rt=map(.$timestamp, ~ ymd_hms(.x, quiet=FALSE))) %>% # с этим подходом ~ в 8 раз медленнее
mutate(rt=ymd_hms(timestamp, quiet=FALSE)) %>%
mutate(hour=as.numeric(map2(.$rt, .$tz, ~ format(.x, "%H", tz=.y)))) %>%
mutate(wkday_text=map2(.$rt, .$tz, ~ weekdays(as.Date(.x, tz=.y)))) %>%
unnest(rt, hour, wkday_text)
Пытаемся выяснить причину такого поведения. Для этого воспользуемся инструментом профилирования кода. Заинтересованные могут в качестве старта посмотреть детали на сайте Profvis: Profvis — Interactive Visualizations for Profiling R Code [3], а также ознакомиться с видео:
Результат профилирования выглядит следующим образом:
Видно, что основное время уходит на многократные преобразования POSIXct
, POSIXlt
. Этому есть определённые объяснения, с отдельными позициями можно ознакомиться здесь: «Why are lubridate functions so slow when compared with as.POSIXct?» [6].
Но для нас основных выводов несколько:
POSIXct
, обладающих одинаковым tz (т.е. мы не будем вынуждены передавать time-zone как параметр);lubridate
, когда при соблюдении определённых форматов текста конвертация chr->POSIXct
осуществляется не базовыми функциями R, а многократно оптимизированными (см. пакет fasttime
).dplyr::do
используем purrr::map2
.Незначительно модифицировав код получаем ускорение почти в 25 раз. (при прогоне на большем массиве данных выигрыш получается почти в 100 раз).
Код выглядит следующим образом:
attacks_raw <- read_csv("./data/eventlog.csv", col_types="ccc", progress=interactive()) %>%
slice(1:20000)
parseDFDayParts <- function(df, tz) {
real_times <- ymd_hms(df$timestamp, tz=tz, quiet=TRUE)
tibble(wkday=weekdays(as.Date(real_times, tz=tz)),
hour=as.numeric(format(real_times, "%H", tz=tz)))
}
attacks <- attacks_raw %>%
group_by(tz) %>%
nest() %>%
mutate(res=map2(data, tz, parseDFDayParts)) %>%
unnest()
Одной из ключевых «фишек» является группировка по time-zone с последующей векторизацией обработки.
Бизнес-кейс также является тривиальным. Есть массив временных данных, характеризующих время отклика на действия пользователя.
Необходимо посчитать APDEX индекс [7] для оценки удовлетворённости пользователей работой приложения.
# Разбиваем, все выполненные операции на 3 категории:
# N – общее количество произведённых операций
# NS - количество итераций, которые выполнены за менее чем целевое время [0 – Т]
# NF – количество операция, которые выполнены за (Т – 4Т]
# (т.е от целевого времени до целевого времени умноженного на 4)
# Индекс APDEX = (NS + NF/2)/N.
В качестве стартовой точки используем ужасающие результаты подсчета метрики по скользящему окну средствами базового R. Для выборки в 2000 записей время расчёта составляет почти 2 минуты!
apdexf <- function(respw, T_agreed = 1.2) {
which(0 < respw$response_time & respw$response_time < T_agreed)
apdex_N <- length(respw$response_time) #APDEX_total
apdex_NS <- length(which(respw$response_time < T_agreed)) # APDEX_satisfied
apdex_NF <- length(which(T_agreed < respw$response_time & respw$response_time < 4*T_agreed)) # APDEX_tolerated F=4T
apdex <- (apdex_NS + apdex_NF/2)/apdex_N
return(apdex)
}
dt = dminutes(15)
df.apdex = data.frame(timestamp=numeric(0), apdex=numeric(0))
for(t in seq(from = start_time, by = dt, length.out = floor(as.duration(end_time - start_time)/dt))){
respw <- subset(mydata, t <= timestamp & timestamp < t+dt)
df.apdex <- rbind(df.apdex, data.frame(timestamp = t, apdex = apdexf(respw)))
}
Пропускаем все промежуточные шаги, сразу отмечаем места для оптимизации.
satisfiedtoleratedfrustrated
является только функцией времени отклика, но не времени измерения или окна расчёта — на вынос.data.frame
передается в функцию по ссылке, операции с data.frame
внутри функции могут приводить к созданию дубликата объекта, а это есть значительные накладные расходы. Оптимизация операций с использованием особенностей методов dplyr
позволяет минимизировать количество копирования данных внутри функции, что существенно увеличивает производительность. С особенности методов dplyr
, а также средствами контроля объектов, в частности, функцией dplyr::changes()
можно ознакомиться в статье «Data frame performance» [8]. Ровно поэтому модель отдельных колонок со статусом времени отклика гораздо предпочтительнее одной колонки с типом статуса времени отклика.POSIXct
+ сдвижка по времени — весьма накладная процедура, чтобы выполнять её внутри цикла.dplyr
с внешней параметризацией. Подобное ухищрение позволяет сократить затраты на вычисления и используемую память, но несколько усложняет синтаксис за счёт возврата от NSE [Non-Standard evaluation] к SE [Standard evaluation]. (filter
--> filter_
). В качестве вводной информации про SENSE можно ознакомиться здесь: «Non-standard evaluation» [9].apdexf2 <- function(df, window_start, cur_time) {
t_df <- df %>%
filter_(lazyeval::interp(~ timestamp>var, var=as.name("window_start"))) %>%
filter_(lazyeval::interp(~ timestamp<=var, var=as.name("cur_time")))
# так быстрее на порядок, чем summarise
s <- sum(t_df$satisfied)
t <- sum(t_df$tolerated)
f <- sum(t_df$frustrated)
(s + t/2)/(s + t + f)
}
t_agreed <- 0.7
mydata %<>%
mutate(satisfied=if_else(t_resp <= t_agreed, 1, 0)) %>%
mutate(frustrated=if_else(t_resp > 4*t_agreed, 1, 0)) %>%
mutate(tolerated=1-satisfied-frustrated) %>%
mutate(window_start=timestamp-minutes(15))
time_df <- mydata %>%
select(window_start, timestamp)
mydata %<>%
select(timestamp, satisfied, tolerated, frustrated)
mydata$apdex <- map2(time_df$window_start, time_df$timestamp,
~ apdexf2(mydata, .x, .y)) %>%
unlist()
Проведя подобную оптимизацию получаем время обработки ~ 3.5 сек. Ускорение почти в 35 раз и достигнуто оно просто незначительным переписыванием кода!
Но и это не предел. Приведённая реализация осуществляет расчёт скользящим окном для по каждой отдельной метрике. Измерения же могут приходить в произвольные моменты времени. Но на практике такая точность избыточна, вполне достаточно загрублять вычисление до 1-5 минут групповых интервалов. А это значит, что объем вычислений может уменьшиться ещё раз в 10, с пропорциональным сокращением времени вычисления.
В заключение хочу отметить, что существующий на настоящий момент набор пакетов и инструментов действительно позволяет писать на R компактно и быстро. В случае, когда и этого будет недостаточно, есть ещё резерв в виде Rcpp
— написания отдельных функций на C++.
Предыдущий пост: «R в enterprise задачах. Хитрости и трюки» [10]
Автор: i_shutov
Источник [11]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/data-mining/247142
Ссылки в тексте:
[1] «Bob Rudis, Making Faceted Heatmaps with ggplot2»: https://rud.is/projects/facetedheatmaps.html
[2] dplyr::do() is now basically deprecated in favour of the purrr approach: https://twitter.com/hadleywickham/status/719542847045636096
[3] Profvis — Interactive Visualizations for Profiling R Code: https://rstudio.github.io/profvis/
[4] 2016 Shiny Developer Conference Videos: Profiling and performance — Winston Chang: https://www.rstudio.com/resources/webinars/shiny-developer-conference/
[5] rconf2017: Understand Code Performance with the profiler – Winston Chang: https://www.rstudio.com/resources/videos/understand-code-performance-with-the-profiler/
[6] «Why are lubridate functions so slow when compared with as.POSIXct?»: http://stackoverflow.com/questions/10645815/why-are-lubridate-functions-so-slow-when-compared-with-as-posixct
[7] APDEX индекс: http://www.apdex.org/
[8] «Data frame performance»: https://cran.r-project.org/web/packages/dplyr/vignettes/data_frames.html
[9] «Non-standard evaluation»: https://cran.r-project.org/web/packages/dplyr/vignettes/nse.html
[10] «R в enterprise задачах. Хитрости и трюки»: https://habrahabr.ru/post/322066/
[11] Источник: https://habrahabr.ru/post/322890/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.