Русский
Русский
English
Статистика
Реклама

R

IT Service Health Monitoring средствами R. Взгляд под иным углом

23.03.2021 00:21:32 | Автор: admin

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


Ключевые слова: cmdb, multi-agent sumulation, monte-carlo, ml.


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


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


Постановку детально расписывать не будем, в интернете можно найти на любой вкус и кошелек. Реферативно выглядит следующим образом (изначально придумали ITSM консультанты):


  • есть CMDB (конфигурационная база элементов ИТ). Она содержит описание элементов и связей между ними (граф);
  • есть система мониторинга, которая как-то снимает показания физических воплощений CI элементов;
  • есть какой-то бизнес-сервис, который базируется на инфраструктурных элементах (сервера, приложения, API, тесты, ...);
  • связь между сервисом и элементами обычно описывается деревом и называется ресурсно-сервисной моделью (РСМ);
  • у инфраструктурных элементов есть свои параметры (KPI) по которым с учетом топологической связности надо посчитать здоровье бизнес-сервиса (health/KQI).

Типовую картинку РСМ возьмем с документации одного из апологетов этой темы (HP).


РСМ. Исходник был здесь:https://docs.microfocus.com/OMi/10.62/Content/OMi/images/ServiceHealth.png


Как делается обычно


Типовая процедура внедрения РСМ состоит из:


  • составления списка сервисов;
  • составления списка ресурсов;
  • составления топологической связи между ресурсами;
  • составления правил пропагандирования состояний и метрик.

Все делается консультантами вручную. Особо интересная работа подгонять весовые коэффициенты пропагандирования метрик (событий) с нижних уровней на верхнии. В случае сложных РСМ редко удается пронаблюдать получение удовлетворительного решения.


Технические ссылки:



Альтернативный план


Ручное исполнение задача в 2021 году выглядит уныло и тоскливо. Но у нас под руками есть компьютер. Можно попробовать применить его по назначению.


План


  • фаза multi-agent sumulation: рассматриваем ресурсы как самостоятельных агентов, обладающих свойствами (входные параметры) и коммуницирующих друг с другом (связи в дереве РСМ);
  • фаза itsm: прописываем любым удобным нам способом поведение на уровне отдельных агентов (функцию выхода от состояний входа);
  • фаза monte-carlo: генерим подходящее подмножество входных состояний всех объектов и проводим расчет отклика MAS структуры;
  • фаза ml: сворачиваем полученный data.frame ансамбль правил (rule fit, см Modern Rule-Based Models by Max Kuhn), объясняемый представителям от бизнеса;
  • фаза prod: полученные ml матрицы загоняем в кремний и получаем event propagation в режиме реального времени на одном смартфоне.

При чем тут R?


В целом, альтернативный план можно делать на чем угодно. Хоть на листочках с карандашом в руке. Но если хочется сэкономить силы и время, то на R можно сделать добрую половину задачи кодом в один экран. И поможет нам в этом Shiny. Да, Shiny, но без Shiny.


Писать multi-agent simulation нудно и есть масса мест для ошибок. С другой стороны у нас есть механизм реактивного программирования в Shiny, который обеспечивает коммуникацию между объектами. Воспользуемся им, см. подсказки в Reactive programming in R by Joe Cheng, DSC 2014


Пример идеи в коде, связка трех нод-агентов nodeA -> nodeB -> nodeC.


MAS на коленке
library(tidyverse)library(magrittr)library(shiny)library(foreach)library(iterators)options(shiny.suppressMissingContextError=TRUE)makeReactiveBinding("nodeA")nodeA$in_1 <- NULLnodeA$in_2 <- NULLnodeA$out <- reactive(nodeA %$% (in_1 + in_2))makeReactiveBinding("nodeB")nodeB$in_1 <- reactive(nodeA$out())nodeB$in_2 <- NULLnodeB$out <- reactive(nodeB %$% (in_1() + in_2))makeReactiveBinding("nodeC")nodeC$in_1 <- reactive(nodeB$out())nodeC$in_2 <- NULLnodeC$out <- reactive(nodeC %$% (in_1() * in_2))df <- tidyr::expand_grid(val1 = 0:5, val2 = 0:5, val3 = 0:5, val4 = 0:5) %>%  # результат прямого расчета для демо-сверки  mutate(direct_res = (val1 + val2 + val3) * val4)res <- foreach(it = iter(df, by = "row"), .combine = "c") %do%  {    nodeA$in_1 <- it$val1    nodeA$in_2 <- it$val2    nodeB$in_2 <- it$val3    nodeC$in_2 <- it$val4    shiny:::flushReact()    nodeC$out()  }df$mas_res <- res

Далее натянуть на data.frame ансамбль деревьев (или gbm или еще что...) и сделать прогноз можно двумя строчками. При этом формулу отклика для каждого агента можно писать любыми доступными средствами. В силу того, что состояния агентов в этой задаче ограничены, можно вместо формул создать конфигурационный excel (пример ниже), который понятен любому менеджеру и не требует никаких споров. Считаете, что в строчке #7 на выходе должно быть 'bad'? Пишем и даже не спорим.


Конфигурация агента для менеджеров


Собственно все, задача решена, кино кончилось.


Предыдущая публикация Немного о параллельных вычислениях в R.

Подробнее..

R, Монте-Карло и enterprise задачи, часть 2

05.04.2021 12:20:40 | Автор: admin

Как ни парадоксально, но пока еще нередко в enterprise встречаются задачи, отличные от построения еще одного личного кабинета, еще одного мониторинга или еще одного документооборота. Если немного подумать, а не хвататься сразу кодировать или искать специализированное ПО, то можно написать компактное, весьма элегантное и быстрое решение, используя метод Монте-Карло .


Задачи в Enterprise достаточны компактны для перебора и не требует точности 100 знаков после запятой. Не ракеты или реакторы запускаем и не научную теорию всего строим.


Рассмотрим далее на примере одной из нестандартных задач.


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


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


Задача имеет корни в IoT для сельского хозяйства. А именно, расстановка датчиков на картофельном поле с круговой схемой полива. Попросим у гугла немного картинок: Разгадка тайны круглых полей: всё интересней, чем ты думаешь!. Нужно обеспечить нужные характеристики покрытия mesh сети, учитывая допустимые расстояния между соседями. При этом надо оптимизировать бюджет и выдать gps координаты на расстановку датчиков и сформировать кратчайшую схему обхода.


Решение


Подключаем пакеты


Пакеты
library(tidyverse)library(magrittr)library(scales)library(data.table)library(tictoc)library(stringi)library(arrangements)library(ggforce)

Декомпозиця


Сначала попробуем сформировать этапы.


  1. Берем N датчиков.
  2. Ищем оптимальную расстановку.
  3. Если характеристики не достигнуты, увеличиваем N на 1. Повторяем процедуру.
  4. Для найденной расстановки ищем оптимальный маршрут обхода.

План вроде простой. Но как будем решать? Напрашивается метод Монте-Карло.


Функции-хелперы


Оформим функцию для визуального отображения расположения зондов.


Визуализация поля
drawDisk <- function(df) {  # отрисуем расположение точек и действующие силы  # если силы не заданы, создадим их по умолчанию равными 0  for(col in c("force_x", "force_y")){    if (!(col %in% names(df))) df[, col] <- 0  }  ggplot(data = df, aes(x = x, y = y)) +    ggforce::geom_circle(aes(x0 = 0, y0 = 0, r = 1, colour = "red"),                          inherit.aes = FALSE) +    geom_point(size = 2, fill = "black", shape = 21) +    geom_text(aes(label = idx), hjust = 0.5, vjust = -1) +    # рисуем векторное поле    geom_segment(aes(xend = x + force_x / 10, yend = y + force_y / 10),                  colour = "red",                  arrow = arrow(length = unit(0.2,"cm")), size = 0.6) +     xlim(-1.5, 1.5) +    ylim(-1.5, 1.5) +    coord_fixed() +    labs(x = "Ось X", y = "Ось Y") +    theme_bw()}

Генерация первичной расстановки


Конечно же, и для этой задачи можно использовать Монте-Карло. Но только далеко не факт, что на малых выборках подберется оптимальная конфигурация. На больших выборках при неограниченном количестве состояний (а пространство круга содержит бесконечное количество точек) и неудачном коде не хватит никаких вычислительных ресурсов.


Но есть выход можем вспомнить физику.


Предположим, что мы накидаем в круглую плоскую чашку N одинаково заряженных шариков. Они либо найдут устойчивую конфигурацию с минимум энергии, либо возникнут колебания. Непосредственно в этой задаче с большой вероятностью будет найдено стабильное расположение (из практики). Попробуем провести такую же симуляцию для расстановки зондов. Наделим каждый зонд единичным зарядом с единичной массой, радиус нашего картофельного поля выберем единичным. Для того, чтобы зонды не разлетелись в бесконечность, вокруг нашего поля поставим заряженных неподвижных охранников. И проведем симуляцию для случайной первичной расстановки.


Генерация первичной расстановки
# Генерим точки-зонды внутри окружности единичного радиуса.# Считаем, что все частицы единичного заряда, поэтому его опускаемcharges_dt <- tibble(idx = 1:13) %>%  mutate(angle = runif(n(), min = 0, max = 2*pi),          r = runif(n(), min = 0, max = 1),         x = r * cos(angle), y = r * sin(angle)) %>%  select(idx, x, y) %>%  setDT(key = "idx")# для сходимости задачи генерируем также зафиксированные точки на внешней окружностиkeepers_dt <- max(charges_dt$idx) %>%   {tibble(idx = (. + 1):(. + 40))} %>%  mutate(angle = (idx - 1) * (2 * pi / n()),         x = 1.3 * cos(angle), y = 1.3 * sin(angle)) %>%  select(idx, x, y) %>%  setDT(key = "idx")

Посмотрим, как расположились зонды при первичной установке


Визуализируем
full_dt <- rbindlist(list(charges_dt, keepers_dt))drawDisk(full_dt)

Поиск оптимального расположения


Здесь задействуем физику, расчитаем малое перемещение частиц за счет действия на них cилы электростатического взаимодействия (закон Кулона).
Для упрощения задачи будем считать


  • каждую новую итерацию как движение из статичного состояния (как-бы случайная расстановка зондов);
  • все зонды обладают единичным зарядом и единичной массой.

$s = at^2/2 = (F/m)t^2/2$


Для малых изменений получаем $\delta s = F \delta t$


Будем итеративно двигать, пока равнодействующие сил на зонды не станут меньше определенного порога.


Поиск оптимального расположения
max_force <- 10tic("Balancing charges")# Определяем точность моделирования!# Будем двигать заряды пока они не застабилизируются # Максимальная равнодействуюущая будет близка к 0 # и точки не стали разлетаться в бесконечностьwhile (max_force > 0.05 & nrow(charges_dt[x^2 + y^2 > 1.2]) == 0) {  # общий пул координат частиц на текущую итерацию  full_dt <- rbindlist(list(charges_dt, keepers_dt), fill = TRUE)  ff <- function(x0, y0){    # сила взаимодействия -- 1/r2, заряды единичные;    # проекция силы на оси, sqrt(r2) -- гипотенуза    # суперпозиция векторов даст результирующее воздействие на частицу    dx = full_dt$x - x0    dy = full_dt$y - y0    r2 = dx^2 + dy^2    # na.rm исключает NaN в т.ч.    list(sum(-dx * r2^(-1.5), na.rm = TRUE),         sum(-dy * r2^(-1.5), na.rm = TRUE))  }    # проведем расчет сил, итерируем по каждой "неприбитой гвоздями" точке  charges_dt[, c("force_x", "force_y") := ff(x0 = x, y0 = y), by = idx]  # определим максимальную силу, действующую на частицу  max_force <- charges_dt[, max(sqrt(force_x^2 + force_y^2), na.rm = TRUE)]  force_scale = if_else(max_force > 1, 1 / max_force / 1e2, 1/ max_force / 5e2)  # проводим передвижение точек  charges_dt %>%    .[, `:=`(x = x + force_x * force_scale,              y = y + force_y * force_scale)]}toc()full_dt <- rbindlist(list(charges_dt, keepers_dt), fill = TRUE)

Оптимизация маршрута обхода


Для выбора оптимального маршрута опять же используем Монте-Карло. Ряд соображений:


  • для генерации подмножества обходов используем комбинаторные функции;
  • для каждого зонда добавим фиктивные точки входа и выхода, которые будут находиться на окружности вдоль радиуса, проходящего через зонд (кратчайший маршрут);
  • для сокращения временных затрат сделаем однократный расчет матриц расстояний.

Оптимизация маршрута обхода
optimizePath <- function(dt) {  # попробуем оптимизировать маршрут обхода по предоставленным точкам  # 1. Выбираем в качестве начальной точки датчик, максимально близко расположенный к краю поля  dt[, r0 := sqrt(x^2+y^2)] %>%    setorder(-r0)  n1 <- dt[1, idx]  # теперь проводим симуляцию различных вариантов расстановки сенсоров  # получаем последовательность номеров и убираем n1, его будем принудительно ставить первым  points_in <- dt[idx != n1, idx]  # для каждой точки добавим еще ближайшую точку выхода   # (перпендикуляр к окружности, которая единичного радиуса)  augment_tbl <- dt %>%    mutate_at("idx", `*`, -1) %>%    mutate(r0 = sqrt(x^2 + y^2)) %>%    mutate_at(vars(x, y), ~(.x/r0)) %>%    bind_rows(dt) %>%    select(idx, x, y)  # однократно посчитаем матрицу расстояний между зондами  ll_tbl <- unique(augment_tbl$idx) %>%    tidyr::expand_grid(l = ., r = .) %>%    filter(l != r, (l > 0 | r > 0)) %>%    # построим уникальный идентификатор ребра    rowwise() %>%    # mutate(m = list(sort(c(l, r))))    mutate(edge_id = stri_c(sort(c(l, r)), collapse = "=")) %>%    ungroup() %>%    distinct(edge_id, .keep_all = TRUE) %>%    # подтягиваем координаты левой точки    left_join(select(augment_tbl, idx, l_x = x, l_y = y), by = c("l" = "idx")) %>%    # подтягиваем координаты левой точки    left_join(select(augment_tbl, idx, r_x = x, r_y = y), by = c("r" = "idx")) %>%    mutate(s = sqrt((l_x - r_x)^2 + (l_y - r_y)^2)) %>%    arrange(l, r)  points_seq <- arrangements::permutations(v = points_in, replace = FALSE,                                        layout = "column", nsample = 5000)  # добавляем точку входа в качестве первой и соотв. точку выхода в качестве последней  routes_lst <- points_seq %>%     rbind(-n1, n1, ., -tail(., 1)) %>%    as.data.frame() %>% as.list()  # превращаем все пути обхода в последовательности ребер  routes_dt <- data.table(route_id = seq_along(routes_lst), route = routes_lst) %>%    .[, .(from = unlist(route)), by = route_id] %>%    .[, to := shift(from, type = "lead"), by = route_id] %>%    # выкидываем все терминальные точки    na.omit() %>%    # строим нормализованный идентификатор ребра    .[, edge_id := stri_c(sort(unlist(.BY)), collapse = "="), by = .(from, to)] %>%    .[, .(route_id, edge_id)] %>%    # подтянем информацию о длине ребра из справочника    .[as.data.table(ll_tbl), s := i.s, on = "edge_id"]  # считаем длину маршрутов, оставляем кратчайший  best_routes <- routes_dt[, .(len = sum(s)), by = route_id] %>%    setorder(len) %>%    head(10) %T>%    print()  # сформируем ТП-10 лучших маршрутов  best_routes %>%    select(route_id) %>%    mutate(idx = routes_lst[route_id]) %>%    tidyr::unnest(idx) %>%    left_join(augment_tbl) %>%    tidyr::nest(data = -route_id) %>%    left_join(best_routes)}

Получаем табличку подобного рода


    route_id      len 1:     2070 8.332854 2:     2167 8.377680 3:     4067 8.384417 4:     3614 8.418678 5:     5000 8.471521 6:     4495 8.542041 7:     2233 8.598278 8:     4430 8.609391 9:     2915 8.61604810:     3380 8.695547

И посмотрим результат размещения


Визуализируем
tic("Route optimization")best_tbl <- optimizePath(charges_dt)toc()best_route_tbl <- best_tbl$data[[1]]full_dt <- rbindlist(list(best_route_tbl, keepers_dt), fill = TRUE)gp <- drawDisk(full_dt) +  # добавим маршрут обхода  geom_path(arrow = arrow(type = "closed"), data = best_route_tbl)gp

Маршрут обхода


Формирование задания


У нас есть пачка оптимальных расстановок, минимизированных по обходу. Переводим условные координаты в GPS, формируем маршрут обхода и выдаем GPS трекер агроному. Можно ставить зонды.


Полезные трюки


Как всегда, затронем вопросы производительности. Если писать не думая, то можно отправить машину на счет на часы или дни. tidyverse подход оказывается медленнее в $10^3$-$10^4$ раз. В приведенном выше коде расчеты по блокам занимают доли секунды. Этого достаточно для обычной задачи, но если нужно быстрее, то можно делать вставки на C++. В целом, скоростные характеристики достигаются результат рядом мер и методик.


  1. Для небольших циклических расчетов накладные расходы на разыменовывание переменных даже в data.table могут оказаться значительными. Base R в блоке поиска оптимального расположения дает выйгрыш на порядок.
  2. Если задачу можно распараллелить, то надо применять функциональные подходы. Проще будет сделать последующее распараллеливание.
  3. Для однородных величин работа с матрицами оказывается на несколько порядков быстрее работы с data.frame. Связано это со схемой выделения памяти и адресации к элементам. Про матрицы незаслуженно забывают при погружении в tidyverse.
  4. Все, что можно посчитать однократно и оформить в виде справочной таблицы, должно быть посчитано заранее.
  5. Монте-Карло очень хороший подход. Быстрое первичное применение может дать полезный результат, а также взглянуть на решение задачи и, возможно, найти какие-то упрощения и аналитики.
  6. Не стесняемся использовать методы аналогии. Они могут позволить построить упрощенную модель исходной задачи, которая вычислительно существенно проще исходной и легко перекладывается на Монте-Карло.

Предыдущая публикация Дети, русский язык и R.

Подробнее..

R и работа со временем. Что за кулисами?

29.04.2021 18:18:03 | Автор: admin

Даты и время являются весьма непростыми объектами:


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

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


Совсем краткое резюме для смартфоночиталей: на больших объемах данных используем только POSIXct с дробными долями секунд. Будет хорошо, понятно, быстро.


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


Стандарты для специфицирования дат и времени


ISO 8601 Data elements and interchange formats Information interchange Representation of dates and times is an international standard covering the exchange of date- and time-related data.


Базовые методы для работы с датой


Дата


Sys.Date()print("-----")x <- as.Date("2019-01-29") # в UTCprint(x)tz(x)str(x)dput(x)print("-----")dput(as.Date("1970-01-01")) # ! origin

Вывод в консоль
## [1] "2021-04-29"## [1] "-----"## [1] "2019-01-29"## [1] "UTC"##  Date[1:1], format: "2019-01-29"## structure(17925, class = "Date")## [1] "-----"## structure(0, class = "Date")

Нестандартный формат даты при инициализации должен специфицироваться специально


as.Date("04/20/2011", format = "%m/%d/%Y")

## [1] "2011-04-20"

Время


В R применяются два базовых типа времени: POSIXct и POSIXlt.
Внешние представления POSIXct и POSIXlt выглядят похожими. А внутренние?


z <- Sys.time()glue("Внешнее представление",      "POSIXct - {z}",      "POSIXlt - {as.POSIXlt(z)}", "---", .sep = "\n")glue("Внутреннее представление",      "POSIXct - {capture.output(dput(z))}",      "POSIXlt - {paste0(capture.output(dput(as.POSIXlt(z))), collapse = '')}",     "---", .sep = "\n")# Получение отдельных элементов даты/времени базовыми средствамиglue("Год: {year(z)} \nМинуты: {minute(z)}\nСекунды: {second(z)}\n---")

Вывод в консоль
## Внешнее представление## POSIXct - 2021-04-29 15:18:04## POSIXlt - 2021-04-29 15:18:04## ---## Внутреннее представление## POSIXct - structure(1619698684.50764, class = c("POSIXct", "POSIXt"))## POSIXlt - structure(list(sec = 4.50764489173889, min = 18L, hour = 15L,     mday = 29L, mon = 3L, year = 121L, wday = 4L, yday = 118L,     isdst = 0L, zone = "MSK", gmtoff = 10800L), class = c("POSIXlt", "POSIXt"), tzone = c("", "MSK", "MSD"))## ---## Год: 2021 ## Минуты: 18## Секунды: 4## ---

Сразу делаем заключение, что для серьезной работы с данными (более 10 строк с временем), про POSIXlt забываем как про страшный сон.


POSIXct по своей сути является оберткой для unixtimestamp, количество секунд (миллисекунд) с некоей нулевой точки (обычно за 0 полагают 01.01.1970). Делаем ставку в работе именно на него.


Полезный инструмент online преобразование времени в unixtimestamp:



Sys.time()z <- 1548802400as.POSIXct(z, origin = "1970-01-01")                # localas.POSIXct(z, origin = "1970-01-01", tz = "UTC")    # in UTC

Вывод в консоль
## [1] "2021-04-29 15:18:04 MSK"## [1] "2019-01-30 01:53:20 MSK"## [1] "2019-01-29 22:53:20 UTC"

Работа с долями секунды


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


  • по рекомендациям ISO, с долями секунд в виде дробной части (ISO 8601-2019);
  • с какими-нибудь другими разделителями;
  • как отдельное поле.

Объекты класса POSIXct могут хранить и проводить вычисления с дробными секундами, но по умолчанию при выводе на печать дробные части округляются из-за чего могут возникнуть надуманные ограничения. Проверяем и смотрим:


x <- ymd_hms("2014-09-24 15:23:10")xx + 0.5x + 0.5 + 0.6options(digits.secs=5)x + 0.45756options(digits.secs=0)x

Вывод в консоль
## [1] "2014-09-24 15:23:10 UTC"## [1] "2014-09-24 15:23:10 UTC"## [1] "2014-09-24 15:23:11 UTC"## [1] "2014-09-24 15:23:10.45756 UTC"## [1] "2014-09-24 15:23:10 UTC"

Вернемся к логам, сформируем тестовый набор данных и посмотрим на различные варианты работы со временем.


options(digits.secs=5)# generate datadf <- data.frame(  timestamp = as_datetime(    round(runif(20, min = now() - seconds(10), max = now()), 0),     tz ="Europe/Moscow")) %>%  mutate(ms = round(runif(n(), 0, 999), 0)) %>%  mutate(value = round(runif(n(), 0, 100), 0))dput(df)# сортируем "в лоб"df %>%  arrange(timestamp, ms)options(digits.secs=0)

Вывод в консоль
## structure(list(timestamp = structure(c(1619698677, 1619698680, ## 1619698676, 1619698682, 1619698675, 1619698682, 1619698679, 1619698679, ## 1619698684, 1619698683, 1619698684, 1619698677, 1619698682, 1619698683, ## 1619698675, 1619698676, 1619698685, 1619698681, 1619698683, 1619698681## ), class = c("POSIXct", "POSIXt"), tzone = "Europe/Moscow"), ##     ms = c(418, 689, 729, 108, 226, 843, 12, 370, 5, 581, 587, ##     691, 102, 79, 640, 284, 241, 85, 329, 936), value = c(63, ##     44, 63, 45, 29, 34, 80, 85, 42, 76, 94, 89, 34, 80, 1, 66, ##     29, 81, 15, 98)), class = "data.frame", row.names = c(NA, ## -20L))


# "умное" преобразование# [magrittr aliases](http://personeltest.ru/aways/magrittr.tidyverse.org/reference/aliases.html)df2 <- df %>%  mutate(timestamp = timestamp + ms/1000) %>%  # mutate_at("timestamp", ~`+`(. + ms/1000)) %>%  select(-ms)df2 %>% arrange(timestamp)


# сравним подходыdt <- as.data.table(df2)bench::mark(  naive = dplyr::arrange(df, timestamp, ms),  smart = dplyr::arrange(df2, timestamp),  dt = dt[order(timestamp)],  check = FALSE,  relative = TRUE,  min_iterations = 1000)

## # A tibble: 3 x 6##   expression   min median `itr/sec` mem_alloc `gc/sec`##   <bch:expr> <dbl>  <dbl>     <dbl>     <dbl>    <dbl>## 1 naive       11.9   11.8      1         1.06     1   ## 2 smart       11.1   11.0      1.06      1        1.06## 3 dt           1      1       11.6     494.       1.22

Парсинг данных с миллисекундами.


data <- c("05102019210003657", "05102019210003757", "05102019210003857")dmy_hms(stri_c(stri_sub(data, to = 14L), ".", stri_sub(data, from = 15L)), tz = "Europe/Moscow")# Измерение скорости различных вариантовdata2 <- data %>%  sample(10^6, replace = TRUE)bench::mark(  stri_sub = stri_c(stri_sub(data2, to = 14L), ".", stri_sub(data2, from = 15L)),  stri_replace = stri_replace_first_regex(data2, pattern = "(^.{14})(.*)", replacement = "$1.$2"),  re2_replace = re2_replace(data2, pattern = "(^.{14})(.*)", replacement = "\\1.\\2", parallel = TRUE))

Вывод в консоль
## [1] "2019-10-05 21:00:03 MSK" "2019-10-05 21:00:03 MSK"## [3] "2019-10-05 21:00:03 MSK"## # A tibble: 3 x 6##   expression        min   median `itr/sec` mem_alloc `gc/sec`##   <bch:expr>   <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>## 1 stri_sub        214ms    222ms      4.10   22.89MB     5.47## 2 stri_replace    653ms    653ms      1.53    7.63MB     0   ## 3 re2_replace     409ms    413ms      2.42   15.29MB     1.21

Пакет lubridate


x <- ymd(20101215)print(x)class(x)

## [1] "2010-12-15"## [1] "Date"

Магия lubridate


ymd(20101215) == mdy("12/15/10")

## [1] TRUE

df <- tibble(first = c("Иван", "Петр", "Алексей"),             last = c("Иванов", "Петров", "Сидоров"),             birthday_str = c("31-10-06", "2/4/2007", "1 June, 2005")) %>%  mutate(birthday = dmy(birthday_str))df


А что делать, если время может поступать в частично обрезанном формате?


# управляем отображением форматов парсинга в lubridateoptions(lubridate.verbose = TRUE)# базовый формат даты: д.м.гdf <- tibble(time_str = c("08.05.19 12:04:56", "09.05.19 12:05", "12.05.19 23"))lubridate::dmy_hms(df$time_str, tz = "Europe/Moscow")print("---------------------")lubridate::dmy(df$time_str, tz = "Europe/Moscow")

## [1] "2019-05-08 12:04:56 MSK" NA                       ## [3] NA                       ## [1] "---------------------"## [1] NA NA NA

Разрешим вариативность определенной глубины


# управляем отображением форматов парсинга в lubridateoptions(lubridate.verbose = TRUE)lubridate::dmy_hms(df$time_str, truncated = 3, tz = "Europe/Moscow")

## [1] "2019-05-08 12:04:56 MSK" "2019-05-09 12:05:00 MSK"## [3] "2019-05-12 23:00:00 MSK"

# управляем отображением форматов парсинга в lubridateoptions(lubridate.verbose = TRUE)# базовый формат даты: д.м.гdf <- tibble(date_str = c("08.05.19", "9/5/2019", "2019-05-07"))

Пробуем провести конвертацию


# пробуем первый вариантglimpse(dmy(df$date_str))print("---------------------")# пробуем второй вариантglimpse(ymd(df$date_str))print("---------------------")

##  Date[1:3], format: "2019-05-08" "2019-05-09" NA## [1] "---------------------"##  Date[1:3], format: "2008-05-19" NA "2019-05-07"## [1] "---------------------"

Что делать? Вариант, конечно, ужасен, но что-то можно поделать.


df %>%  mutate(date = dplyr::coalesce(dmy(date_str), ymd(date_str)))

tab4


df1 <- dfdf1$date <- dmy(df1$date_str)idx <- is.na(df1$date)print("---------------------")idxdf1$date[idx] <- ymd(df1$date_str[idx])print("---------------------")df1

## [1] "---------------------"## [1] FALSE FALSE  TRUE## [1] "---------------------"

tab5


Еще пакеты


Еще пакеты на "посмотреть" и поизучать:



Арифметические операции с POSIXct


Разность


options(lubridate.verbose = FALSE)date1 <- ymd_hms("2011-09-23-03-45-23")date2 <- ymd_hms("2011-10-03-21-02-19")# какова разница между этими датами?as.numeric(date2) - as.numeric(date1) # как мы помним, разница в секундах(date2 - date1) %>% dput()difftime(date2, date1)difftime(date2, date1, unit="mins")difftime(date2, date1, unit="secs")

## [1] 926216## structure(10.7200925925926, class = "difftime", units = "days")## Time difference of 10.72009 days## Time difference of 15436.93 mins## Time difference of 926216 secs

Периоды


date1 <- ymd_hms("2019-01-30 00:00:00")date1date1 - days(1)date1 + days(1)date1 + days(2)

## [1] "2019-01-30 UTC"## [1] "2019-01-29 UTC"## [1] "2019-01-31 UTC"## [1] "2019-02-01 UTC"

А теперь более сложный пример добавляем месяцы


date1 - months(1)date1 + months(1) # УПС!!!

## [1] "2018-12-30 UTC"## [1] NA

Есть выход. Но операции не коммутативны, это надо помнить.


date1 %m-% months(1)date1 %m+% months(1)date1 %m+% months(1) %m-% months(1)

## [1] "2018-12-30 UTC"## [1] "2019-02-28 UTC"## [1] "2019-01-28 UTC"

Нюансы временных зон


date1 <- ymd_hms("2019-01-30 01:00:00")date1 %T>% print() %>% dput()with_tz(date1, tzone = "Europe/Moscow") %T>% print() %>% dput()force_tz(date1, tzone = "Europe/Moscow") %T>% print() %>% dput()

Вывод в консоль
## [1] "2019-01-30 01:00:00 UTC"## structure(1548810000, class = c("POSIXct", "POSIXt"), tzone = "UTC")## [1] "2019-01-30 04:00:00 MSK"## structure(1548810000, class = c("POSIXct", "POSIXt"), tzone = "Europe/Moscow")## [1] "2019-01-30 01:00:00 MSK"## structure(1548799200, class = c("POSIXct", "POSIXt"), tzone = "Europe/Moscow")

Работа только с временми значениями


Что делать, если у нас есть только время, а даты не указаны? Не проблема, нам поможет пакет hms. Такие данные представляются как периоды.


hms_str <- "03:22:14"as_hms(hms_str)dput(as_hms(hms_str))print("-------")x <- as_hms(hms_str) * 15xstr(x)# seconds_to_period(period_to_seconds(x))seconds_to_period(x) %T>% dput() %>% print()

Вывод в консоль
## 03:22:14## structure(12134, units = "secs", class = c("hms", "difftime"))## [1] "-------"## Time difference of 182010 secs##  'difftime' num 182010##  - attr(*, "units")= chr "secs"## new("Period", .Data = 30, year = 0, month = 0, day = 2, hour = 2, ##     minute = 33)## [1] "2d 2H 33M 30S"

БД и временне данные


Одна из больших засад при работе с временнми данными в БД неизвестность или неполная осведомленность о механике и логике работы конкретных таблиц. Не всегда есть возможность посмотреть запросы по которым они строились или же текст функций.
В современных БД (далее будем подразумевать Clickhouse) время, как правило, хранится как unixtimestamp в UTC. Ну или возможны иные варианты, но все они крутятся вокруг количества единиц времени относительно некоей реперной точки.


Потенциальные сложности и засады:


  • При запросе у БД колонки времени под ее капотом может происходить масса метаморфоз. БД сериализует timestamp, при этом могут оказать свое влияние параметры временных зон из БД, ОС, поля, смежного поля, переменных окружения.
  • При получении данных на клиентской стороне вмешивается драйвер (серия драйверов и врапперов). При развертывании времени замешивается логика драйверов, параметры локали ОС, языковые и временные параметры среды, значение переменных окружения и отражение лунного света в болоте.
  • В поле unixtimestamp разработчики могут помещать отнюдь не UTC время, а московское. Или иное (сюрприз!).
  • В БД может быть агрегация и партиционирование по дате, вычисляемой на основании поля timestamp. В силу расхождения в трактовке временных зон, данные за день Х вполне могут уехать в партиции X-1 или X+1, что необходимо учитывать при построении быстрого запроса к БД.

В общем, вероятность получить предсказуемый результат в любой произвольной среде исполнения близок к 0.


Чтобы избежать этого и параллельно получить еще массу преимуществ достаточно перейти на ручное управление.
Суть заключается в переводе дат в числовой формат на стороне базы и обратное преобразование во время (там, где надо) на стороне клиента. Такое решение не сильно обременительно, зато дает дает массу преимуществ:


  • полный контроль реальных временнх меток на всех этапах, включая выявлении косяков разработчиков и специфики настройки БД;
  • возможность сверки реально получаемы показателей с ожидаемыми;
  • прецизионное управление временными зонами для корректной трактовки;
  • корректное преобразование времени в даты (с учетом таймзон);
  • схождение суточных агрегатов;
  • возможность интеграции дробных долей секунд в единый double;
  • сокращение временнх затрат на сериализацию и передачу по сети;
  • общее увеличение производительности.

Трюк по экономии памяти и времени исполнения без потери информации


-- диалект ClickHouseSELECT DISTINCT    store, pos,    timestamp, ms,    concat(toString(store), '-', toString(pos)) AS pos_uid,    toFloat64(timestamp) + (ms / 1000)          AS timestamp

flog.info(paste("SQL query:", sql_req))tic("Загрузка из CH")raw_df <- dbGetQuery(conn, stri_encode(sql_req, to = "UTF-8")) %>%  mutate_if(is.character, `Encoding<-`, "UTF-8") %>%  as_tibble() %>%  mutate_at(vars(timestamp), anytime::anytime, tz = "Europe/Moscow") %>%  mutate_at("event", as.factor)flog.info(capture.output(toc()))DBI::dbDisconnect(conn)

Хелпер для детального анализа занимаемой data.frame памятью


# сводка по объемам данныхdf -> as_tibble(_df) %>%  map(pryr::object_size) %>%   unlist() %>%   enframe() %>%   arrange(desc(value)) %>%  mutate_at("value", fs::as_fs_bytes) %>%  mutate(ratio = formattable::percent(value / sum(value), 2)) %>%  add_row(name = "TOTAL", value = sum(.$value))

Повторно полезные ссылки по форматам и калькуляторам, необходимым при анализе путей следования дат в ИС и БД



Форматирование дат


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


Привязка к рабочим неделям


df <- seq.Date(from = as.Date("2021-01-01"),                to = as.Date("2021-05-31"),                by = "2 days") %>%  # sample(20, replace = FALSE) %>%  tibble(date = .)

# формируем композитное представление год/месяц/номер недели# способ 1df %>%  mutate(month_num = stri_c(lubridate::year(date),                             sprintf("%02d", lubridate::month(date)),                             sep = "/"),         week_num = stri_c(lubridate::isoyear(date),                            sprintf("%02d", lubridate::isoweek(date)),                            sep = "/")  )

tab6


# формируем композитное представление год/месяц/номер недели# способ 2, заодно добавим день недели# особое внимание обращаем, что текстовые поля генерятся согласно текущей локали!!!df %>%  mutate(month_num = format(date, "%Y/%m (%a) ISO week %V"))

tab7


# формируем композитное представление год/месяц/номер недели# способ 3, заодно добавим день недели# хелпер по преобразованию формата strptime (ISO 8601) в ICU# https://man7.org/linux/man-pages/man3/strptime.3.htmlstri_datetime_fstr("%Y/%m (%a) week %V")# ggthemes::tableau_color_pal("Tableau 20")(20) %>% scales::show_col()# особое внимание обращаем, что мы можем управлять локалью самостоятельно!!!df %>%  mutate(    month_num_ru = stri_datetime_format(      date, "yyyy'/'MM' ('ccc') week 'ww", locale = "ru", tz = "UTC"),    month_num_en = stri_datetime_format(      date, "yyyy'/'MM' ('ccc') week 'ww", locale = "en", tz = "UTC"))

tab8


Дни недели


Пишем дни недели в различных локалях. Не зависит от платформы исполнения.


stri_datetime_format(today(), "LLLL", locale="ru@calendar=Persian")stri_datetime_format(today(), "LLLL", locale="ru@calendar=Indian")stri_datetime_format(today(), "LLLL", locale="ru@calendar=Hebrew")stri_datetime_format(today(), "LLLL", locale="ru@calendar=Islamic")stri_datetime_format(today(), "LLLL", locale="ru@calendar=Coptic")stri_datetime_format(today(), "LLLL", locale="ru@calendar=Ethiopic")stri_datetime_format(today(), "dd MMMM yyyy", locale="ru")stri_datetime_format(today(), "LLLL d, yyyy", locale="ru")

## [1] "ордибехешт"## [1] "ваисакха"## [1] "ияр"## [1] "рамадан"## [1] "бармуда"## [1] "миазия"## [1] "29 апреля 2021"## [1] "апрель 29, 2021"

Собственное форматирование дат по осям графиков


Иногда возникает необходимость собственного форматирования меток осей. Ниже пример по созданию такой функции


# сгенерируем тестовые данныеmap_tbl <- tibble(  date = as_date(Sys.time() + rnorm(10^3, mean = 0, sd = 60 * 60 * 24 * 7))) %>%  mutate(store = stri_c(sample(c("A", "F", "Y", "Z"), n(), replace = TRUE),                        sample(101:105, n(), replace = TRUE))) %>%  mutate(store_fct = as.factor(store)) %>%  mutate(fail_ratio = abs(rnorm(n(), mean = 0.3, sd = 1)))

my_date_format <- function (format = "dd MMMM yyyy", tz = "Europe/Moscow") {  scales:::force_all(format, tz)  # stri_datetime_fstr("%d.%m%n%A")  # stri_datetime_fstr("%d.%m (%a)")  function(x) stri_datetime_format(x, format, locale = "ru", tz = tz)}# такой же график, но в развертке по горизонталиgp <- map_tbl %>%  ggplot(aes(x = date, y = store_fct, fill = fail_ratio)) +  geom_tile(color = "white", size = 0.1) +  # scale_fill_distiller(palette = "RdYlGn", name = "Fail Ratio", label = comma) +  # scale_fill_distiller(palette = "RdYlGn", name = "Fail Ratio", guide = guide_legend(keywidth = unit(4, "cm"))) +  scale_fill_distiller(palette = "RdYlGn", name = "Fail Ratio") +  scale_x_date(breaks = scales::date_breaks("1 week"), labels = my_date_format("dd'.'MM' ('ccc')'")) +  coord_equal() +  labs(x = NULL, y = NULL, title = "Средний % сбоев по дням") +  theme_minimal() +  theme(plot.title = element_text(hjust = 0)) +  theme(axis.ticks = element_blank()) +  theme(axis.text = element_text(size = 7)) +  theme(axis.text.x = element_text(angle = 90, vjust = 0.5)) +  theme(legend.position = "bottom") +  theme(legend.key.width = unit(3, "cm"))gp

heatmap


Тесты производительности


Простая математика


Создадим тестовый набор записей


base_df <- tibble(  start = Sys.time() + rnorm(10^3, mean = 0, sd = 60 * 24 * 3)) %>%  mutate(finish = start + rnorm(n(), mean = 100, sd = 60)) %>%  mutate(user_id = sample(as.character(1000:1100), n(), replace = TRUE)) %>%  arrange(user_id, start)dt <- as.data.table(base_df, key = c("user_id", "start")) %>%  .[, c("start", "finish") := lapply(.SD, as.numeric),     .SDcols = c("start", "finish")]

Сам бенчмарк
df <- group_by(base_df, user_id)bench::mark(  dplyr_v1 = df %>% transmute(delta_t = as.numeric(difftime(finish, start, units = "secs"))) %>% ungroup(),  dplyr_v2 = ungroup(df) %>% transmute(delta_t = as.numeric(difftime(finish, start, units = "secs"))),  dplyr_v3 = dt %>% transmute(delta_t = finish - start),  dt_v1 = dt[, .(delta_t = finish - start), by = user_id],  dt_v2 = dt[, .(delta_t = finish - start)],  check = FALSE # all_equal работает более корректно)

## # A tibble: 5 x 6##   expression      min   median `itr/sec` mem_alloc `gc/sec`##   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>## 1 dplyr_v1      4.3ms   4.86ms      200.   103.1KB    11.4 ## 2 dplyr_v2     2.17ms   2.46ms      380.    17.9KB     6.24## 3 dplyr_v3     1.67ms   1.77ms      527.    29.8KB     8.51## 4 dt_v1       410.4us  438.7us     2139.    90.8KB     8.35## 5 dt_v2       304.4us  335.3us     2785.   264.6KB     8.38

У меня данные хранятся в формате год/месяц/число. Мне не все нужны, а только суббота, как мне отфильтровать?


# https://stackoverflow.com/questions/16347731/how-to-change-the-locale-of-r# https://jangorecki.gitlab.io/data.cube/library/stringi/html/stringi-locale.htmldf <- as.Date("2020-01-01") %>%   seq.Date(to = . + months(4), by = "1 day") %>%  tibble(date = .) %>%  mutate(wday = lubridate::wday(date, week_start = 1),         wday_abb_rus = lubridate::wday(date, label = TRUE, week_start = 1),         wday_abb_enu = lubridate::wday(date, label = TRUE, week_start = 1, locale = "English"),         wday_stri = stringi::stri_datetime_format(date, "EEEE", locale = "en"))# оставим только субботыfilter(df, wday == 6)

tab9


Предыдущая публикация R vs Python в продуктивном контуре.

Подробнее..

Как отлаживать код в RStudio и создавать новый проект на R

22.03.2021 12:19:24 | Автор: admin
Новогодним подарком в этом году стали для меня новая команда и проект на языке R, о котором в тот момент я знал немного. Поначалу было трудно и не понятно, но время шло, картинка прояснялась. С чем-то удалось разобраться, что-то пришлось принять как есть. И вот, спустя два с половиной месяца работы на R, я решил поделиться опытом и рассказать о своих первых шагах в этом проекте. Я не буду описывать все свои душевные муки и эмоции, которые переполняли меня в процессе освоения этого очень интересного языка, а сосредоточусь на технической стороне вопроса. Цель моей статьи рассказать о том, как отлаживать код в RStudio и создавать новый проект на R.

Первое с чем пришлось мне столкнуться это отладка приложения. В RStudio есть возможность выделить отдельные участки кода и запустить их. Это очень помогает при работе с R markdown, так как в них, в режиме Debug, нельзя поставить точку останова. А выделить строчки и запустить их можно где угодно.

запуск строчки кода

Причем переменные, которые будут вызываться в этих строчках, он берет из глобального окружения. Получается, что для проверки того, как работает та или иная функция достаточно создать глобальные переменные, построчно запускать код из этих функций и смотреть что происходит. Я работаю в RStudio, где на вкладке Global Environment можно увидеть значения этих переменных.

панель глобального окружения

Создают или меняют значения этих переменных через Console.

Console

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

кнопка Source

Есть в RStudio и классический Debug режим. В нем присутствует возможность ставить Break Points, с возможностью запуска в режиме отладки и функция browser(), встретив которую R прерывает выполнение кода, позволяя отладить приложение. Но в нашем проекте это не получило широкого распространения из-за работы с R markdown.

Следующее с чем я столкнулся в R это два типа проектов: обычный проект (New Project) и проект типа package (R Package). Когда я пришёл в команду, там был некий микс из этих 2-х. Вроде был Package, но он не собирался и запускали его через RScrtipt. Сейчас, благодаря усилиям моих коллег, у нас работающий R Package.

image

Обычный проект (New Project) предлагает написать R-скрипт файлы, где один файл подключается к другому через функцию source(). Таким образом, при запуске скрипта получается как бы один очень большой файл, в который включены все файлы проекта. Это не всегда удобно и не очень гибко.

кнопка Source

В отличие от обычного проекта, проект типа R Package предлагает нам написать библиотеку функций на R, которую потом можно будет установить на любую машину и вызывать эти функции внутри своего R-скрипт файла. Есть правда один нюанс. Функции доступны только из R-скрипта. Поэтому, прежде чем начать с ними работать, нужно будет создать такой скрипт и уже в нем прописать вызовы этих функций. Запускается он в консоли с помощью команды: Rscript. Чтобы это работало нужно в переменных окружения прописать путь к файлу Rscript.exe. На моей машине этот путь выглядит так: C:\Program Files\R\R-4.0.3\bin. При создании своих функций в проекте типа Package, в режиме разработки, следует пользоваться функцией load_all(), которая подтягивает все изменения в память. Если ей не пользоваться, то при всяком изменении кода в проекте, для того чтобы эти изменения вступили в силу, надо запустить процесс инсталляции, что R делает не быстро.

Теперь о проекте R-Package: В отличие от простого проекта, он содержит некую обязательную структуру и специальные файлы. Это:

  • файл DESCRIPTION с описанием пакета,
  • папка man для описания функций,
  • файл NAMESPACE со списком доступных функций создаваемого пакета,
  • папка с названием R, в которой должен лежать ваш код на языке R
  • файл .Rbuildignore со списком того, что не входит в пакет при его сборке
  • файл .Rhistory, который хранит историю команд в консоли
  • .RData хранит содержимое вкладки Environment, точнее данные которые были загружены в память при работе с проектом

.Rhistory и .RData присутствуют как в проекте типа Package, так и в обычном проекте. Ещё можно создать папку inst для дополнительных ресурсов и папку test для unit тестов. Более детально с тем, как устроен R package и почему именно так он работает вы можете ознакомиться по ссылке.

При создании проекта типа R Package используют утилиту roxygen2. Она помогает создать документацию для вашего пакета. Идея в том, что вы описываете каждую функцию прямо в коде, а утилита уже сама переносит это описание в папку man, конвертируя в необходимый формат и добавляет информацию о функциях в файл NAMESPACE. Подробнее о roxygen2 тут.

Ещё есть полезные пакеты для настройки проекта, которые используют практически во всех инструкциях по его созданию, как минимум во всех тех, что я нашёл в Интернет:

  • devtools основной пакет, в котором собранно большинство команд для работы с проектом в их упрощенном виде
  • usethis пакет помощник, упрощающий выполнение многих рутинных операций
  • testthat пакет для написания Unit тестов
  • covr пакет для проверки кода на покрытие unit тестами

Все общедоступные пакеты с их описанием и с документацией к языку R хранятся в CRAN The Comprehensive R Archive Network (http://personeltest.ru/aways/cran.r-project.org/ )

Для удобства работы, в RStudio уже встроены средства проверки вновь создаваемого проекта (Check Package) и его тестирования (Test Package).

Build меню

Вроде бы всё, что хотел, рассказал, но лучше один раз увидеть, чем сто раз прочитать. Ниже видео о том, как начать работать с R:

  1. Начало работы с R на RStudio. Проект на языке R или пакет на языке R
  2. Создание проекта R package вместе с Unit tests и documentation
  3. Запуск и отладка R кода для проекта R Package

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

R в руках маркетолога. Делаем когортный анализ своими руками

02.05.2021 12:15:03 | Автор: admin

В маркетинге очень популярен когортный анализ. Его популярность вызвана, скорее всего, легкостью алгоритма и вычислений. Никаких серьезных математических концепций в основе нет, элементарная математика, выполняемая в excel. С точки зрения получения инсайтов гораздо интереснее анализ дожития.


Тем не менее, считаем, что есть такая задача и ее надо решить. Искать какие-либо пакеты и готовые функции неинтересно математика проста, параметров настройки масса. Ниже возможный пример реализации (без особой фиксации на скорость исполнения), всего кода на пару десятков строк.


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


Немного кода


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


Создание тестового набора
# генерируем данные на 15 недельset.seed(42)events_dt <- tibble(user_id = 1000:9000) %>%  mutate(birthday = Sys.Date() + as.integer(rexp(n(), 1/10))) %>%  rowwise() %>%  mutate(timestamp = list(as_datetime(birthday) + 24*60*60 * (     rexp(10^3, rate = 1/runif(1, 2, 25))))) %>%  ungroup() %>%  unnest(timestamp) %>%  # режем длинные хвосты в прошлом и в будущем  filter(timestamp >= quantile(timestamp, probs = 0.1),         timestamp <= quantile(timestamp, probs = 0.95)) %>%  mutate(date = as_date(timestamp)) %>%  select(user_id, date) %>%  setDT(key = c("user_id", "date")) %>%  # оставим только уникальные по датам события  unique()

Посмотрим на получившееся интегральное распределение


ggplot(events_dt, aes(date)) +  geom_histogram()


Шаг 1. Формируем справочник пользователей


В настоящем примере справочник будет тривиальным и содержит только "дату рождения", т.е. дату, когда мы первый раз встретились с ним. Установка ключей для data.table объекта приводит к физической сортировке данных в порядке появления ключей.


Формируем справочник пользователей
users_dict <- events_dt[, .(birthday = head(date, 1)), by = user_id] %>%  # для последующей сортировки оставим дату начала недели  .[, week_start := floor_date(.BY[[1]], unit = "week"), by = birthday] %>%    # переведем даты рождения в номера когорт  .[, cohort := stri_c(        lubridate::isoyear(.BY[[1]]),         sprintf("%02d", lubridate::isoweek(.BY[[1]])),         sep = "/"), by = week_start]# посмотрим на распределение дат, нам нужен разброс для красивой картинкиas_tibble(janitor::tabyl(users_dict, birthday))


Шаг 2. Подготовим разметку в терминах когортного анализа


Совсем за скоростью пока не гонимся.


Составим справочник когорт. Для сокращения преобразований и обеспечения последующей сортировки.


Строим когортное представление в data.frame
cohort_dict <- unique(users_dict[, .(cohort, week_start)])cohort_tbl <- users_dict[events_dt, on = "user_id"] %>%  # посчитаем удаленность событий от даты рождения в терминах недель  .[, rel_week := floor(as.numeric(difftime(date, birthday, units = "week")))] %>%  # оставим только 10 недель  .[rel_week <= 9] %>%  # редуцируем до уникальных пользователей  unique(by = c("user_id", "cohort", "rel_week")) %>%  # считаем агрегаты в терминах когорт и недель  .[, .N, by = .(cohort, rel_week)] %>%  .[, rate := N/max(N), by = cohort]

Шаг 3. Визуализируем


Вариант 1. ggplot


Визуализация ggplot
# вариант ggplotdata_tbl <- cohort_tbl %>%  # вернем числовые показатели когорт для сортировки  left_join(cohort_dict)data_tbl %>%  mutate(cohort_group = forcats::fct_reorder(cohort, week_start, .desc = TRUE)) %>%  ggplot(mapping = aes(x = rel_week, y = cohort_group, fill = rate)) +  geom_tile()  +  geom_text(aes(label = N), colour = "darkgray") +  labs(x = "Недели существования когорты",       y = "Неделя появления когорты",       fill = "Количество\nпользователей",       title = "graph_title") +  scale_fill_viridis_c(option = "inferno") +  scale_x_continuous(breaks = scales::breaks_width(1)) +  theme_minimal() +  theme(panel.grid = element_blank())


Вариант 2. gt


Для оформления используем тот факт, что у нас всегда по две строки на когорту и они отсортированы в нужном порядке.


Визуализация gt
# подготовим табличку-подложкуdata_tbl <- cohort_tbl %>%  pivot_longer(cols = c(N, rate)) %>%  pivot_wider(names_from = rel_week, values_from = value) %>%  # вернем числовые показатели когорт для сортировки  left_join(cohort_dict) %>%  arrange(week_start, desc(name))odd_rows <- seq(1, to = nrow(data_tbl), by = 2)even_rows <- seq(2, to = nrow(data_tbl), by = 2)tab <- data_tbl %>%  mutate(cohort = if_else(rep(c(TRUE, FALSE), length.out = nrow(.)),                           cohort, "")) %>%  select(-name, -week_start) %>%  gt(rowname_col = "cohort") %>%  fmt_percent(columns = matches("[0-9]+"),               rows = odd_rows,               decimals = 0, pattern = "<big>{x}</big>") %>%  fmt_missing(columns = everything(),               missing_text = "---") %>%  tab_stubhead(label = "Неделя появления когорты") %>%  tab_spanner(label = "Неделя существования когорты",              columns = everything()) %>%  tab_header(title = "Развертка") %>%  data_color(columns = everything(),             colors = scales::col_numeric(palette = "inferno",                                          domain = c(0, 1),                                           alpha = 0.6,                                          na.color = "lightgray")) %>%  tab_options(    table.font.size = "smaller",    data_row.padding = px(1),    table.width = pct(75)  ) %>%  tab_style(    style = list(      cell_fill(color = "white"),      cell_text(style = "italic"),      cell_borders(sides = "bottom")    ),    locations = cells_body(      columns = everything(),      rows = even_rows)  ) %>%  tab_style(    style = list(      cell_borders(sides = "top")    ),    locations = cells_body(      columns = everything(),      rows = odd_rows)  )tab


Каркас приведен, прочее каждый может модифицировать под себя.


Предыдущая публикация R и работа со временем. Что за кулисами?.

Подробнее..

Storytelling отчет против BI, прагматичный подход

08.05.2021 10:22:37 | Автор: admin

Проблематика


Когда говорят про отчеты к данным (неважно, какая тема) все хотят гибкие дашборды, МНОГО дашбордов, играют конкурсы про BI, выдумывают разные сложные требования и кейсы, отсматривают массу вендоров и решений, разбиваются на непримиримые лагеря и на 100% уверены, что это то, без чего жизнь на работе тяжела, уныла и печальна.


Так ли это? По описанию очень сомнительно (похоже на серебряную пулю), а практика дает подтверждение отнюдь не так.


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


Что в реальности


В реальности все оказывается совсем по другому. Сразу исключим из рассмотрения настоящих аналитиков, которым этот инструмент может раскрыться в полной мере (остановимся чуть позже). В подавляющем большинстве случаев не нужны менеджерам эти дашборды с сотней ручек. Им нужны ограниченные срезы для решения повседневных задач. Основным драйвером за BI и дашборды являются страх и лень. Страх остаться с данными один на один и невозможностью ответить на какой-либо вопрос. Страх пойти по пути, который не отлит в корпоративных стандартах как бетонный монолит. Лень задуматься о истинных потребностях обычных пользователей и альтернативных способах решениях.


Откуда такие утверждения? Дело в том, что отчетные квесты больше похожи на сеансы психотерапии. Каждый раз приходится медленно пробираться через наслоение индуцированных утверждений и убеждений, чтобы добраться до истинной потребности и после этого объяснить ее компьютеру. Последний точно не умеет и не понимает общих слов и смутных ощущений. У него только 0 и 1.


Итак, какие есть существенные выгоды у storytelling отчетов, выполненных на базе технологии RMarkdown. Для ознакомления с возможностями RMarkdown можно проглядеть галерею. Далее будем по умолчанию подразумевать HTML форму отчета, как наиболее удобную для динамичной работы.


Выгода 1. Предоставление пользователю информации в готовом к употреблению виде


Storytelling отчет строится под потребности конкретной группы потребителей. Содержание документа, набор визуальных представлений, порядок подачи материала, поясняющий и связующий текст все предназначено для того, чтобы пользователь мог читать отчет как книгу. Материал раскрывается подробно, от общего к частному. Можно быстро перейти к нужной главе. Если структура отчета давно знакома, то можно быстро просматривать ключевые точки в известных местах. При этом подача материала устроена таким образом, что нет нужды задавать фильтры, листать менюшки, загружать настройки (как это делают в дашбордах). Машина уже это выполнила на этапе подготовки. Все собрано сразу в готовом к употреблению виде, достаточно лишь скроллить вверх/вниз и брать информацию в работу.


Выгода 2. Полная отвязка от инфраструктурных ограничений


В большинстве случаев storytelling отчет нужен по закрытому дню и генерировать его надо один раз в сутки. Оптимальный вариант ночная offline генерация всех необходимых отчетов для текущего рабочего дня. Классическое окно для генерации 2:00-7:00. Когда предыдущий день закрыт и переходные процессы закрытия во всех внутренних ИС завершились. Отсюда четыре существенных плюса:


  • offline генерация на серверной стороне позволяет не заморачиваться на скорость исполнения. В случае интерактивного анализа отклик должен измеряться десятками миллисекунд, иначе пользователи начинают жаловаться на торможение системы. Здесь же мы можем считать секунды. Снижение к требованиям по железу колоссальное.
  • можно использовать любой сложности алгоритмы, в т.ч. весь спектр ML инструментов.
  • нет понятие лицензий на доступ, нет непредсказуемой конкурентности. планируете задания последовательно-параллельно, опираясь на доступные вам аппаратные средства.

Выгода 3. Полная отвязка отчета от источников данных


Будучи собранным, storytelling отчет является самодостаточной и законченной сущностью. Это единый html контейнер, который содержит в себе всю необходимую информацию. Отсюда три существенных плюса:


  • готовый отчет можно как рассылать пользователям по почте, так и предоставлять доступ через корпоративные порталы. пользователь сможет работать с ним везде и за пределом корп.сети, и даже в бункере в режиме полного оффлайна.
  • отчет это html файл. тривальными средствами можно обеспечить историческое хранение на любую требуемую глубину со стоимостью, близкой к 0.
  • в исторической части хорошо известная проблема, когда отлаженный неизменный отчет нельзя собрать на исторических данных так, как он собирался бы тогда, когда эта дата была. Связано это с изменчивостью данных, изменчивостью ландшафта, эволюцией ИС и систем, динамичностью справочников. В тривиальном случае оргструктура любой компании динамична и она сегодня не такова, какой была 3 года назад. Обычному отчету никогда не вернуться в прошлое. А storytelling отчет это след папоротника в известняке.

Выгода 4. Динамическая генерация, основанная на предоставленных данных


Классический отчет жестко зафиксированный состав элементов и их форма. В случае со storytelling отчетом можно использовать подходы динамической генерации, когда сама ткань отчета генерируется на основе структуры полученных данных. Тем самым возможна почти любая вариативность, при этом текст отчета остается связным и без пустых мест. Чуть детальнее с кодом можно посмотреть в публикации R Markdown. Как сделать отчет в условиях неопределенности?.


Также можно параметризировать RMarkdown отчет и получать различные представления на различных данных, используя один и тот же код.


Выгода 5. Динамический контент


Представление html позволяет добавить такой объем динамичности, что пользователь может забыть, что это offline. Включение js виджетов позволяет обеспечить динамичные таблицы, графики. Средствами html и js можно располагать части отчета на закладках, обеспечивать кросснавигацию и оглавление, управлять видимостью объектов. Использование механизмов кросскоммуникации js виджетов позволяет делать связанные обновления элементов. Более конкретно:



Выгода 6. Безопасный доступ к данным


Очень часто возникают требования по управлению доступом к данным. Та или иная группа пользователей должна иметь возможность строить отчет только по своему подмножеству. Но весь парадок в том, что все равно остается ряд показателей, которые считаются по всей компании. И если это делать через self-service bi, то потребуется продумывать как это предоставить. Витрины, кубы, выгрузки или еще что-либо.


В случае с Rmarkdown отчетами пользователи вообще не имеют доступа к данным. Все собирается на сервере с контролируемым полным доступом. А в html фиксируются посчитанные агрегаты в соответствии с ролевой моделью.


Выгода 7. Один источник масса представлений


RMarkdown позволяет из единого источника делать совершенно различные финальные представления. html, pdf, doc, pptx.


Также, технология RMarkdown позволяет делать статические сайты (blogdown) и книги (bookdown).


Выгода 8. Вся мощь devops для гарантий корректности


Поскольку Rmarkdown является набором R инструкций, то управление жизненным циклом отчетов получается идентичным управлениею жизненным циклом ПО. Репозиторий, пакетирование, документирование, автотесты, continious integration, code coverage, профилировка узких мест.


Также, RMarkdown отчеты могут самостоятельно на этапе сборки подключаться к любым источникам данных (не обязательно выверенным SQL таблицам) и строить отчет на актуальных в моменте данных.


Выгода 9, 10, X. ......


Выберите из списка...


Заключение


Еще один важный комментарий. Для того, чтобы корректно использовать дашборды или self-analytics bi нужно очень-очень хорошо знать структуру данных, которые используются в процессе рассчета и визуализации и иметь к ним полный доступ. Данные должны быть чистые, консистентые и рафинированные. Аналитик должен обладать навыками проведения расчетов и проверки результатов. Только так можно получить показатели и картинки, которым как-то можно доверять. В противном случае имеем сплошные жареные факты и желтую прессу. Затраты на такого аналитика никак не середнячковые.


P.S.


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


Алармисты уже не дремлют. Gartner вовсю начал предрекать закат BI в том виде как он есть сейчас. Нет поводов не задуматься:



Предыдущая публикация R в руках маркетолога. Делаем когортный анализ своими руками.

Подробнее..

Оценка кредитного портфеля на R

19.05.2021 18:19:31 | Автор: admin

В ходе обсуждений возникла маленькая задачка построить динамику структуры кредитного портфеля (динамика кредитной карты, например). В качестве важной специфики необходимо применять метод FIFO для погашения займов. Т.е. при погашении первыми должны гаситься самые ранние займы. Это накладывает определенные требования на расчет статуса каждого отдельного займа и определения его даты погашения.


Ниже приведен код на R с прототипом подхода. Не более одного экрана кода на прототип и никаких циклов (закладные для производительности и читаемости).


Декомпозиция


Поскольку мы делаем все с чистого листа, то задачу разбиваем на три шага:


  1. Формирование тестовых данных.
  2. Расчет даты погашения каждого займа.
  3. Расчет и визуализация динамики для заданного временнОго окна.

Допущения и положения для прототипа:


  1. Гранулярность до даты. В одну дату только одна транзакция. Если в один день будет несколько транзакций, то надо будет их порядок устанавливать (для соблюдения принципа FIFO). Можно использовать доп. индексы, можно использовать unixtimestamp, можно еще что-либо придумывать. Для прототипа это несущественно.
  2. Явных циклов for быть не должно. Лишних копирований быть не должно. Фокус на минимальное потребление памяти и максимальную производительность.
  3. Будем рассматривать следующие группы задержек: "< 0", "0-30", "31-60", "61-90", "90+".

Шаг 1.


Генерируем датасет. Просто тестовый датасет. Для каждого пользователя сформируем ~ по 10 записей. Для расчетов полагаем, что займ положительное значение, погашение отрицательное. И весь жизненный цикл для каждого пользователя должен начинаться с займа.


Генерация датасета
library(tidyverse)library(lubridate)library(magrittr)library(tictoc)library(data.table)total_users <- 100events_dt <- tibble(  date = sample(    seq.Date(as.Date("2021-01-01"), as.Date("2021-04-30"), by = "1 day"),    total_users * 10,    replace = TRUE)  ) %>%  # сделаем суммы кратными 50 р.  mutate(amount = (runif(n(), -2000, 1000)) %/% 50 * 50) %>%  # нашпигуем идентификаторами пользователей  mutate(user_id = sample(!!total_users, n(), replace = TRUE)) %>%  setDT(key = "date") %>%  # первая запись должна быть займом  .[.[, .I[1L], by = user_id]$V1, amount := abs(amount)] %>%  # для простоты оставим только одну операцию в день,   # иначе нельзя порядок определить и гранулярность до секунд надо спускать  # либо вводить порядковый номер займа и погашения  unique(by = c("user_id", "date"))

Шаг 2. Расчитываем даты погашения каждого займа


data.table позволяет изменять объекты по ссылке даже внутри функций, будем этим активно пользоваться.


Расчет даты погашения
# инициализируем аккумуляторaccu_dt <- events_dt[amount < 0, .(accu = cumsum(amount), date), by = user_id]ff <- function(dt){  # на вход получаем матрицу пользователей и их платежей на заданную дату  # затягиваем суммы займов  accu_dt[dt, amount := i.amount, on = "user_id"]  accu_dt[is.na(amount) == FALSE, accu := accu + amount][accu > 0, accu := NA, by = user_id]  calc_dt <- accu_dt[!is.na(accu), head(date, 1), by = user_id]  # нанизываем обратно на входной data.frame, сохраняя порядок следования  calc_dt[dt, on = "user_id"]$V1}repay_dt <- events_dt[amount > 0] %>%  .[, repayment_date := ff(.SD), by = date] %>%  .[order(user_id, date)]

Шаг 3. Считаем динамику задолженности за период


Расчет динамики
calcDebt <- function(report_date){  as_tibble(repay_dt) %>%    # выкидываем все, что уже погашено на дату отчета    filter(is.na(repayment_date) | repayment_date > !! report_date) %>%    mutate(delay = as.numeric(!!report_date - date)) %>%    # размечаем просрочки    mutate(tag = santoku::chop(delay, breaks = c(0, 31, 61, 90),                               labels = c("< 0", "0-30", "31-60", "61-90", "90+"),                               extend = TRUE, drop = FALSE)) %>%    # делаем сводку    group_by(tag) %>%    summarise(amount = sum(amount)) %>%    mutate_at("tag", as.character)}# Устанавливаем окно наблюденияdf <- seq.Date(as.Date("2021-04-01"), as.Date("2021-04-30"), by = "1 day") %>%  tibble(date = ., tbl = purrr::map(., calcDebt)) %>%  unnest(tbl)# строим графикggplot(df, aes(date, amount, colour = tag)) +  geom_point(alpha = 0.5, size = 3) +  geom_line() +  ggthemes::scale_colour_tableau("Tableau 10") +  theme_minimal()

Можем получить примерно такую картинку.


Один экран кода, как и требовалось.


Предыдущая публикация Storytelling R отчет против BI, прагматичный подход.

Подробнее..

Перевод Clustergram визуализация кластерного анализа на Python

28.05.2021 14:21:34 | Автор: admin

В этой статье, переводом которой мы решили поделиться специально к старту курса о Data Science, автор представляет новый пакет Python для генерации кластерограмм из решений кластеризации. Библиотека была разработана в рамках исследовательского проекта Urban Grammar и совместима со scikit-learn и библиотеками с поддержкой GPU, такими как cuML или cuDF в рамках RAPIDS.AI.


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

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

Маттиас Шонлау предложил другой подход кластерограмму. Кластерограмма это двухмерный график, отражающий потоки наблюдений между классами по мере добавления кластеров. Это говорит вам о том, как перетасовываются ваши данные и насколько хороши ваши сплиты. Тал Галили позже реализовал кластерограмму для k-средних в R. Я использовал реализацию Таля, перенёс ее на Python и создал clustergram пакет Python для создания кластерограмм.

clustergram в настоящее время поддерживает метод k-средних, использование scikit-learn (включая реализацию Mini-Batch) и RAPIDS.AI cuML (если у вас есть GPU с поддержкой CUDA), Gaussian Mixture Model (только scikit-learn) и иерархическую кластеризацию на основе scipy.hierarchy. В качестве альтернативы мы можем создать кластерограмму на основе меток и данных, полученных с помощью альтернативных пользовательских алгоритмов кластеризации. Пакет предоставляет API, подобный sklearn, и строит кластерные диаграммы с помощью matplotlib, что даёт ему широкий выбор вариантов оформления в соответствии со стилем вашей публикации.

Установка

Установить clustergram можно при помощи conda или pip:

conda install clustergram -c conda-forge

или

pip install clustergram

В любом случае вам нужно установить выбранный бэкенд (scikit-learn и scipy или cuML).

from clustergram import Clustergramimport urbangrammar_graphics as uggimport seaborn as snsimport matplotlib.pyplot as pltfrom sklearn.preprocessing import scalesns.set(style='whitegrid')

Давайте рассмотрим несколько примеров, чтобы понять, как выглядит кластерограмма и что с ней делать.

Набор данных о цветке ириса

Первый пример, который мы пытаемся проанализировать с помощью кластерограммы, это знаменитый набор данных о цветке ириса. Он содержит данные по трём видам цветков ириса с измерением ширины и длины чашелистиков, а также и ширины и длины лепестков. Мы можем начать с разведки:

iris = sns.load_dataset("iris")g = sns.pairplot(iris, hue="species", palette=ugg.COLORS[1:4])g.fig.suptitle("Iris flowers", y=1.01)

Похоже, что setosa относительно чётко определённая группа, тогда как разница между versicolor и virginica меньше, поскольку они частично перекрываются (или, в случае ширины чашелистика, полностью).

Итак, мы знаем, как выглядят данные. Теперь мы можем увидеть кластерограмму. Помните, мы знаем, что существует три кластера, и в идеале мы должны быть в состоянии распознать это по кластерограмме. Я говорю в идеале, потому что, даже если есть известные метки, это не значит, что наши данные или метод кластеризации способны различать эти классы.

Давайте начнём с кластеризации методом k-средних. Чтобы получить стабильный результат, мы можем запустить кластерную программу с 1000 инициализаций.

data = scale(iris.drop(columns=['species']))cgram = Clustergram(range(1, 10), n_init=1000)cgram.fit(data)ax = cgram.plot(    figsize=(10, 8),    line_style=dict(color=ugg.COLORS[1]),    cluster_style={"color": ugg.COLORS[2]},)ax.yaxis.grid(False)sns.despine(offset=10)ax.set_title('K-Means (scikit-learn)')

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

Мы ищем разделение, т. е. отвечаем на вопрос, принёс ли дополнительный кластер какое-либо значимое разделение? Шаг от одного кластера к двум большой хорошее и чёткое разделение. От двух до трёх свидетельство довольно хорошего раскола в верхней ветви. Но с 3 по 4 видимой разницы нет, потому что новый четвёртый кластер почти не отличается от существующей нижней ветви. Хотя сейчас она разделена на две части, это разделение не даёт нам много информации. Таким образом, можно сделать вывод, что идеальное количество кластеров для данных Iris три.

Мы также можем проверить некоторую дополнительную информацию, например оценку силуэта или оценку Калинского Харабазса.

fig, axs = plt.subplots(2, figsize=(10, 10), sharex=True)cgram.silhouette_score().plot(    xlabel="Number of clusters (k)",    ylabel="Silhouette score",    color=ugg.COLORS[1],    ax=axs[0],)cgram.calinski_harabasz_score().plot(    xlabel="Number of clusters (k)",    ylabel="Calinski-Harabasz score",    color=ugg.COLORS[1],    ax=axs[1],)sns.despine(offset=10)

По этим графикам можно предположить наличие 34 кластеров по аналогии с кластерограммой, но они не очень убедительны.

Набор данных о пингвинах со станции Палмера

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

penguins = sns.load_dataset("penguins")g = sns.pairplot(penguins, hue="species", palette=ugg.COLORS[3:])g.fig.suptitle("Palmer penguins", y=1.01)

Глядя на ситуацию, мы видим, что перекрытие между видами гораздо выше, чем раньше. Скорее всего, идентифицировать их будет гораздо сложнее. Кроме того, мы знаем, что существует три кластера, но это не означает, что данные способны их различать. В этом случае может быть особенно сложно отличить пингвинов Адели от антарктических пингвинов.

data = scale(penguins.drop(columns=['species', 'island', 'sex']).dropna())cgram = Clustergram(range(1, 10), n_init=1000)cgram.fit(data)ax = cgram.plot(    figsize=(10, 8),    line_style=dict(color=ugg.COLORS[1]),    cluster_style={"color": ugg.COLORS[2]},)ax.yaxis.grid(False)sns.despine(offset=10)ax.set_title("K-Means (scikit-learn)")

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

Можно ли сказать, что их три? Поскольку мы знаем, что их должно быть три... Ну, не совсем. Разница между разделениями 23 и 34 незначительна. Однако здесь виновником является метод K ближайших соседей, а не кластерограмма. Он просто не может правильно кластеризовать эти данные из-за наложений и общей структуры. Давайте посмотрим, как работает смешанная Гауссова модель (Gaussian Mixture).

cgram = Clustergram(range(1, 10), n_init=100, method="gmm")cgram.fit(data)ax = cgram.plot(    figsize=(10, 8),    line_style=dict(color=ugg.COLORS[1]),    cluster_style={"color": ugg.COLORS[2]},)ax.yaxis.grid(False)sns.despine(offset=10)ax.set_title("Gaussian Mixture Model (scikit-learn)")

Результат очень похож, хотя разница между третьим и четвёртым разделениями более выражена. Даже здесь я бы, вероятно, выбрал решение с четырьмя кластерами.

Подобная ситуация случается очень часто. Идеального случая не существует. В конечном счёте нам необходимо принять решение об оптимальном количестве кластеров. Clustergam даёт нам дополнительные сведения о том, что происходит между различными вариантами, как они расходятся. Можно сказать, что вариант с четырьмя кластерами в данных Iris не помогает. Также можно сказать, что пингвины Палмера могут быть сложными для кластеризации с помощью k-средних, что нет решающего правильного решения. Кластерограмма не даёт простого ответа, но она даёт нам лучшее понимание, и только от нас зависит, как мы её [кластерограмму] интерпретируем.

Установить clustergram можно с помощью conda install clustergram -c conda-forge или pip install clustergram. В любом случае вам всё равно придётся установить бэкенд кластеризации: либо scikit-learn, либо cuML. Документация доступна здесь, а исходный код здесь, он выпущен под лицензией MIT.

Если вы хотите поиграть с примерами из этой статьи, блокнот Jupyter находится на GitHub. Вы также можете запустить его в среде interactive binder в браузере. Более подробную информацию можно найти в блоге Тала Галили и оригинальных статьях Матиаса Шонлау.

Вполне понятно, что идеальной кластеризации не существует. И даже сегодня, с учётом всего прогресса искусственного интеллекта, для принятия сложных решений о данных по-прежнему нужен человек. Если вам интересна наука о данных область, в которой такие решения принимаются постоянно вы можете обратить внимание на наш курс о Data Science, где через подкреплённую теорией практику студенты учатся ориентироваться в данных, делать обоснованные выводы и действовать с открытыми глазами.

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Тест Уилкоксона золотая середина для практиков

17.03.2021 12:07:26 | Автор: admin

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

В статье с единой точки зрения обсуждаются три часто встречающихся на практике одновыборочных теста: тест знаков, t-тест и тест Уилкоксона (Signed-Rank Wilcoxon test) непараметрической процедуры, мощность которой сравнима с мощностью t-теста в случае нормально распределенной выборки, и превышает мощность t-теста в случае, если распределение выборки имеет более тяжелые хвосты по сравнению с нормальным распределением.

1. Определим модель для параметра положения (location model) следующим образом. Пусть X_1, X_2,\ldots,X_n обозначает случайную выборку, полученную по следующему закону

X_i=\theta+e_i,

где предполагается, что случайные ошибки e_1,e_2,\ldots,e_n это независимые и одинаково распределенные случайные величины с непрерывной плотностью распределенияf(t), симметричной относительно нуля.

2. При условии симметрии любой параметр положения X_i , включая среднее и медиану, равен \theta . Рассмотрим гипотезу

H_0:\theta=0,~~~H_a:\theta>0.

3. Для проверки данной гипотезы рассмотрим три часто используемых на практике теста: тест знаков, t-тест и тест Уилкоксона.

3.1. Классический тест знаков (sign test) основан на статистике

S=\sum_{i=1}^nsign(X_i),

где sign(t)=-1,0,1 для t<0,t=0,t>0 соответственно. Пусть

S^+=\#_i\{X_i>0\}.

Тогда S=2S^+-n . Здесь предполагается, что ни одно из значений X_i не равно нулю (на практике, равные нулю значения из выборки исключают, а объем выборки n корректируют). При условии H_0 , статистика S^+ имеет биномиальное распределение с числом испытаний n и вероятностью успеха 1/2 . Пусть s^+ наблюдаемая величина S^+ тогда p-value для теста знаков равно P_{H_0}(S^+\geq s^+)=1-F_B(s^+-1;n;0.5) , где F_B(t;n;p) функция биномиального распределения с параметрами n и p (R функция pbinom возвращает значения cdf для биномиального распределения).

Заметим, что в тесте знаков распределение статистики S при нулевой гипотезе H_0 не зависит (свободно) от вида распределения f(t) .

3.2. Следующий традиционный t-тест (t-test) основан на сумме наблюдений. По аналогии можно записать

T=\sum_{i=1}^nsign(X_i)\cdot|X_i|.

Заметим, что распределение статистики T зависит от плотности распределения f(t) . Обычно t-тест записывают в форме t-отношения

t=\frac{\bar{X}}{s/\sqrt{n}},

где \bar{X} и s соответственно, выборочное среднее и стандартное отклонение. Если выборка получена из нормального распределения, то статистика t имеет t-распределение Стьюдента с n-1 степенью свободы. Пусть t_0 наблюдаемое по выборке значение t . Тогда p-value для t-теста равно P_{H_0}(t\geq t_0)=1-F_T(t_0;n-1) , где F_T(t;\nu) функция t-распределения Стьюдента c \nu степенью свободы (R функция pt возвращает значения cdf для t-распределения). Это точное значение p-value в случае нормального распределения, в противном случае это аппроксимация.

3.3. Отличие t-теста от теста знаков состоит в том, что статистика t-теста является функцией расстояний элементов выборки относительно нуля в дополнение к их знакам.

Выбранная нами статистика теста Уилкоксона (signed-rank Wilcoxon test) хороша тем, что использует лишь ранги этих расстояний. Обозначим R|X_i| ранг X_i среди всех |X_1|,\ldots,|X_n| , упорядоченных от меньшего значения к большему. Тогда статистика Уилкоксона имеет вид

W=\sum_{i=1}^nsign(X_i)\cdot R|X_i|.

В противоположность статистике t-теста, статистика W , также как и рассмотренная ранее статистика S при условии нулевой гипотезы H_0 не зависит от вида f(t) .

Распределение статистики W не может быть выведено в виде законченной формулы и при ее расчете используется итерационный алгоритм. Обычно, наряду со статистикой W , составляют сумму рангов положительных элементов выборки W^+ , то есть

W^+=\sum_{X_i>0}R|X_i|=\frac{1}{2}W+\frac{n(n+1)}{4}.

Тогда p-value для теста Уилкоксона равно P_{H_0}(W^+\geq w^+)=1-F_{W^+}(w^+-1;n) , где F_{W^+}(x;n) функция распределения статистики Уилкоксона для выборки размера n (R функция psignrank возвращает значения cdf распределения W^+ ).

4. Техника построения доверительных интервалов широко используется при решении практических задач. Каждый из рассмотренных выше тестов: тест знаков, t-тест и тест Уилкоксона имеет соответствующую оценку и доверительный интервал для параметра положения \theta . Рассмотрим далее имеющиеся результаты.

4.1. Оценкой параметра положения \theta , связанной с тестом знаков является выборочная медиана

\hat{\theta}=med\{X_1,X_2,\ldots,X_n\}.

Для 0<\alpha<1 соответствующий доверительный интервал для \theta с доверительной вероятностью (1-\alpha)100\% задается в виде \left(X_{(c_1+1)},X_{(n-c_1)}\right) , где X_{(i)} i -ая порядковая статистика выборки, c_1 \alpha/2 квантиль биномиального распределения с параметрами n и p=1/2 . Этот доверительный интервал не зависит от вида распределения ошибок e_i . Отметим, что из-за дискретности биномиального распределения для каждого значения n существует ограниченный набор значений\alpha.

4.2. Оценкой параметра положения \theta , связанной с t-тестом является выборочное среднее \bar{X} . Классический доверительный интервал в этом случае имеет вид \bar{X}\pm t_{\alpha/2,n-1}\cdot[s/\sqrt{n}] , где t_{\alpha/2,n-1} \alpha/2 квантиль t-распределения Стьюдента с n-1 степенью свободы. Данный доверительный интервал зависит от вида распределения ошибок e_i .

4.3. Оценкой параметра положения \theta , связанной с тестом Уилкоксона является оценка Ходжеса-Лемана (Hodges-Lehmann)

\hat{\theta}_W=med_{i\leq j}\left\{\frac{X_i+X_j}{2}\right\}.

Парные средние A_{ij}=(X_i+X_j)/2 , i\leq j называются средними Уолша (Walsh averages) выборки. Пусть A_{(1)}<\cdots<A_{(n(n+1)/2)} упорядоченный набор средних Уолша. Тогда (1-\alpha)100\% доверительный интервал для \theta имеет вид \left(A_{(c_2+1)}, A_{(n(n+1)/2-c2)}\right) , где c_2 \alpha/2 квантиль signed-rank Wilcoxon распределения. Этот доверительный интервал не зависит от вида распределения ошибок e_i при условии их симметрии относительно нуля. Отметим, что размах значений W^+ множество \left\{0,1,,n(n+1)/2\right\} имеет порядок n^2 . Поэтому, для умеренных по размеру выборок, тест Уилкоксона менее зависим от дискретного характера распределения статистики критерия, то есть выбранный уровень значимости \alpha в этом случае ближе к найденному.

5. В качестве практического примера рассмотрим данные об объеме продаж (в штуках) для восьми товарных позиций в двух магазинах A и B за неделю. Ответим на вопрос, в каком магазине спрос на товары выше?

Составим выборку, каждый элемент которой представляет собой разницу в продажах соответствующей товарной позиции в магазинах A и B. Пусть \theta характеризует центральное значение выборки. Следующая R сессия показывает результат применения теста Уилкоксона и t-теста для проверки правосторонней гипотезы H_0:\theta=0,H_a:\theta>0.

> Store_A <- c(82, 69, 73, 43, 58, 56, 76, 65)> Store_B <- c(63, 42, 74, 37, 51, 43, 80, 62)> response <- Store_A - Store_B> wilcox.test(response, alternative = "greater", conf.int = TRUE)Wilcoxon signed rank exact testdata:  responseV = 32, p-value = 0.02734alternative hypothesis: true location is greater than 095 percent confidence interval:   1 Infsample estimates:(pseudo)median           7.75 > t.test(response, alternative = "greater", conf.int = TRUE)One Sample t-testdata:  responset = 2.3791, df = 7, p-value = 0.02447alternative hypothesis: true mean is greater than 095 percent confidence interval: 1.781971      Infsample estimates:mean of x      8.75 

Тест Уилкоксона wilcox.test() возвращает статистику W^+ , p-value теста, оценку Ходжеса-Лемана для \theta и 95\% доверительный интервал для \theta . Т-тест t.test() имеет аналогичный синтаксис и результаты. Как видно, обе процедуры отвергают нулевую гипотезу на уровне 0.05 , то есть можно сказать, что спрос на продукцию в магазине A выше.

Подведем итог, из трёх рассмотренных в статье тестов для практического применения рекомендуется тест Уилкоксона. Он требует минимум предположений о характере распределения генеральной совокупности, сравним по мощности с t-тестом в случае нормального распределения и превышает мощность t-теста в случае симметричного непрерывного распределения с более тяжелыми хвостами по сравнению с нормальным распределением.

Подробнее..

Статистически устойчивый анализ данных тест Манна-Уитни-Уилкоксона и Score-функции

21.03.2021 02:21:27 | Автор: admin

В настоящей статье развиваются идеи и распространяются методы, изложенные в прошлой публикации Статистически устойчивый анализа данных: Тест Уилкоксона на случай двух выборок. Это простая, но широко используемая на практике модель, так как даже в более сложных ситуациях целевые показатели часто сопоставляются на двух уровнях.

Анализ модели о сдвиге параметров положения двух генеральных совокупностей начинается с описания свободной от распределения ранговой процедуры Манна-Уитни-Уилкоксона (Mann-Whitney-Wilcoxon, MWW), здесь строятся точечные и интервальные оценки для величины сдвига. Далее кратко описывается метод анализа, основанный на применении score функций и, с его помощью, также проверяется нулевая гипотеза о величине параметре сдвига. В заключение, модель для параметра положения формулируется в виде регрессионной задачи, решение которой также позволяет построить точечную и интервальную оценки для параметра сдвига.

Все изложенные в статье методы проиллюстрированы на сквозном примере, реализованном в виде алгоритмов на языке R.

1.ПустьXиY две непрерывные случайные величины:F(t)иf(t)обозначают функцию (cdf) и плотность (pdf) распределения случайной величиныX, аG(t)иg(t)обозначают соответственно функцию (cdf) и плотность (pdf) случайной величиныY. Будем говорить, чтоXи Y следуют модели для параметра положения (location model), если для некоторого параметра\Delta, -\infty<\Delta<\infty имеем

G(t)=F(t-\Delta),~~~~g(t)=f(t-\Delta).

Параметр\Delta это сдвиг параметра положения случайных величинYиX, например, это может быть разница между медианами или средними (в случае, если средние существуют). Отметим, что предложенная модель предполагает равенство параметров масштаба случайных величинXиY.

2.Рассмотрим две независимые друг от друга выборки, извлеченные в соответствии с выбранной моделью. ПустьX_1,\ldots,X_{n_1} случайная выборка из генеральной совокупности с законом распределенияX(с функциями cdf и pdfF(t)иf(t)соответственно), иY_1,\ldots,Y_{n_2} случайная выборка из генеральной совокупности с законом распределенияY(с функциями cdf и pdfF(t-\Delta)иf(t-\Delta)соответственно). Пусть такжеn=n_1+n_2 размер объединённой выборкиX_1,\ldots,X_{n_1},Y_1,\ldots,Y_{n_2}. Рассмотрим гипотезу

H_0:\Delta=0,~~~H_a:\Delta\neq0.

При этом односторонние альтернативы также допустимы.

Далее изложены методы проверки (точный и асимптотический) данной гипотезы, а также обсуждаются подходы для получения оценки параметра сдвига\Delta: точечной оценки и доверительного интервала.

3.Упорядочим элементы выборки по возрастанию, то есть запишем её в виде вариационного ряда. Каждому элементу ряда поставим в соответствие его номер в ряду ранг. Если несколько элементов ряда совпадают по величине, то каждому из них присваивается ранг, равный среднему арифметическому их номеров. Последний элемент в ранжированной выборке изnэлементов должен иметь рангn. Этот факт можно использовать при проверке правильности ранжирования.

> z <- c(12, 18, 11, 5, 11, 5, 11, 11)> rank(z)[1] 7.0 8.0 4.5 1.5 4.5 1.5 4.5 4.5

ПустьR(Y_i)означает рангY_iсреди элементов объединенной выборки, то есть средиX_1,\ldots,X_{n_1},Y_1,\ldots,Y_{n_2}. Тогда статистика теста Уилкоксона имеет вид

T=\sum_{i=1}^{n_2}R(Y_i)

На практике часто используется эквивалентнаяTследующая статистика Манна-Уитни-Уилкоксона (Mann-Whitney-Wilcoxon, MWW). Рассмотрим набор из всехn_1\cdot n_2разностей вида\left\{Y_j-X_i\right\}и пустьT^+ число положительных таких разностей, то есть

T^+=\#_{i,j}\{Y_j-X_i>0\}.

В этом случае, верно равенство

T^+=T-\frac{n_2(n_2+1)}{2}.

Очевидно, что исследуемую двустороннюю гипотезуH_0следует отвергнуть при малых и больших значениях статистикиT. При условииH_0:\Delta=0обе выборки получены из одной и той же генеральной совокупности, поэтому любые наборы рангов равновероятны (например, вероятность того, что подмножество ранговn_2относится к случайной величинеYравна1/C_n^{n_2}). Следовательно, распределение статистикиTпри нулевой гипотезеH_0не зависит (свободно) от распределения генеральной совокупности и точное значение p-value рассчитывается на основе распределенияT(точном распределении рангов).

4.Оценкой параметра сдвига\Deltaв тесте Манна-Уитни-Уилкоксона является медиана выборки, составленной из всехN_d=n_1\cdot n_2парных разностей вида (оценка Ходжеса-Лемана (Hodges-Lehmann))

\hat{\Delta}_W=\mbox{med}_{i,j}\{Y_j-X_i\}.

ПустьD_{(1)}<D_{(2)}<\cdots<D_{(N_d)}вариационный ряд для таких разностей. Если доверительная вероятность равна1-\alphaиc квантиль уровня\alpha/2распределенияT^+, то есть \alpha/2=P_{H_0}[T^+\leq c] , то интервал\left(D_{(c+1)},D_{(N_d-c)}\right)является(1-\alpha)100\%доверительным интервалом для\Delta. Асимптотическим значение дляcявляется величина

c=\frac{n_1n_2-1}{2}-z_{\alpha/2}\sqrt{\frac{n_1n_2(n+1)}{12},}

которая округляется до ближайшего целого и в общем случае довольно близка к фактическому значению.

5.Для примера применим тест Манна-Уитни-Уилкоксона к двум выборкам из t-распределения c5степенями свободы и значением параметра сдвига \Delta=8 .

> x <- round(rt(11, 5) * 10 + 42, 1)> y <- round(rt(9, 5) * 10 + 50, 1)> x [1] 76.6 41.0 59.3 34.9 29.1 45.0 42.6 31.1 32.4 52.5 47.9> y [1] 58.3 47.2 40.1 45.8 62.0 58.7 64.8 48.1 49.5> wilcox.test(y, x, exact = TRUE, conf.int = TRUE, conf.level = 0.95)Wilcoxon rank sum exact testdata:  y and xW = 72, p-value = 0.09518alternative hypothesis: true location shift is not equal to 095 percent confidence interval: -1.0 18.4sample estimates:difference in location                   10.4

Здесь:T^+=72с p-value0.09518доверительный интервал(-1,18.4)включает истинное значение\Delta=8, точечная оценка равна\hat{\Delta}_W=10.4, доверительная вероятность95\%. По умолчанию p-value основано на точном распределении статистикиT^+для малых выборок (n<50) без хвостов. В случае выбора аргумента exact = FALSE и correct = FALSE (без корректировки на непрерывность) используется асимптотический метод, изложенный далее. В этом случае значение p-value равно0.08738.

> wilcox.test(y, x, exact = FALSE, correct = FALSE)Wilcoxon rank sum testdata:  y and xW = 72, p-value = 0.08738alternative hypothesis: true location shift is not equal to 0

6.Рассмотрим функциюa_\varphi(i)=\varphi(i/(n+1)), где функция\varphi(u)(score функция) определена на интервале(0,1)и

\int_0^1\varphi(u)du=0,~~~\int_0^1\varphi^2(u)du=1.

Например,\varphi_{Ns}(u)=\Phi^{-1}(u), где\Phi^{-1}(u) функция, обратная к функции cdf стандартной нормальной случайной величиныN(0,1). В этом случае преобразование a_{Ns} посредством функции\varphi_{Ns}(Normal score function) ставит в соответствие рангам элементов выборки значения, которые ожидаемы тем, как если бы данные были получены из нормального распределения. Заметим, что в данном определении термин normal score употребляется в смысле rankit, а не в смысле standard score или z-score. Наряду с функцией normal score, возможны другие виды score функций, например\varphi_W(u)=\sqrt{12}[u-(1/2)]задаёт score функцию Уилкоксона.

Для выбранной модели для параметра положения определим следующую функцию переменной\Delta:

S_\varphi(\Delta)=\sum_{j=1}^{n_2}a\varphi[R(Y_j-\Delta)],

гдеa_\varphi некоторая score функция,R(Y_j-\Delta) рангиY_j-\DeltaсредиX_1,\ldots,X_{n_1}и Y_1-\Delta,\ldots,Y_{n_2}-\Delta . В этом случае, статистикуS_\varphi=S_\varphi(0)можно использовать для проверки интересующей нас гипотезы:

H_0:\Delta=0,~~~H_a:\Delta>0.

При условии нулевой гипотезыH_0, случайные величиныXиYраспределены одинаково, отсюда следует, что распределение статистикиS_\varphiнезависимо (свободно) от общего распределения генеральной совокупности.

Хотя точное распределениеS_\varphiи может быть получено численно, обычно используют асимптотическое распределение. В условиях нулевой гипотезыH_0распределениеS_\varphi(0)асимптотически нормально, с математическим ожиданием и дисперсией соответственно:

E_{H_0}[S\varphi(0)]=0,~~~\sigma^2_\varphi=Var_{H_0}[S_\varphi(0)]=\frac{n_1n_2}{n(n-1)}\sum_{i=1}^na_\varphi^2(i).

Это позволяет использовать стандартизованную статистикуz_\varphi=S_\varphi(0)/\sigma_\varphiчтобы отвергнуть гипотезуH_0на уровне значимости\alpha, еслиz_\varphi\geq z_\alpha, гдеz_\alpha квантиль уровня(1-\alpha)стандартного нормального распределения. Двусторонняя и левосторонняя гипотезы проверяются аналогично.

7.В следующей R сессии для данных из рассматриваемого в статье примера представлен расчёт асимптотической статистикиz_\varphiи соответствующее ей значение p-value с использованием score функции Уилкоксона (пакет Rfit).

> x <- c(76.6, 41.0, 59.3, 34.9, 29.1, 45.0, 42.6, 31.1, 32.4, 52.5, 47.9)> y <- c(58.3, 47.2, 40.1, 45.8, 62.0, 58.7, 64.8, 48.1, 49.5)> # Объединим выборки x и y>   z = c(x, y)>   n1 = length(x)>   n2 = length(y)>   n = n1 + n2> # Выберем в качестве score функции функцию Уилкоксона>   scores = Rfit::wscores > # Найдём соответствующие score значения для рангов выборки z >   rs = rank(z)/(n + 1)>   asg = Rfit::getScores(scores, rs)> # Найдём значение статистики Sphi при нулевой гипотезе  >   Sphi = sum(asg[(n1 + 1):n])> # Найдём дисперсию Sphi>   asc = Rfit::getScores(scores, 1:n/(n + 1))>   varphi = ((n1 * n2)/(n * (n - 1))) * sum(asc^2)> # Найдём значения zphi и p-value >   zphi = Sphi/sqrt(varphi)>   alternative = "two.sided">   pvalue <-+     switch(+     alternative,+     two.sided = 2 * (1 - pnorm(abs(zphi))),+     less = pnorm(zphi),+     greater = 1 - pnorm(zphi)+   )> # Выведем результаты>   res <- list(Sphi = Sphi, statistic = zphi, p.value = pvalue)>   with(res, cat("statistic = ", statistic, ", p-value = ", p.value, "\n"))statistic =  1.709409 , p-value =  0.08737528

Таким образом, результаты точногоT^+=72с p-value 0.0952 и асимптотическогоz_W=1.71с p-value0.0874анализа крайне близки: нулевая гипотеза не отвергается на уровне значимости5\%и существует значимая разница в сдвиге параметра положения при уровне значимости10\%.

8.Cформулируем двухвыборочную задачу для параметра положения в виде регрессионной задачи. Пусть\bar{Z}=(X_1,\ldots,X_{n_1},Y_1,\ldots,Y_{n_2})^T,\bar{c} этоn\times1вектор сi-ым элементом0для1\leq i\leq n_1и1дляn_1+1\leq i\leq n=n_1+n_2. Тогда модель для параметра положения можно записать в виде

Z_i=\alpha+c_i\Delta+e_i,

гдеe_1,\ldots,e_n независимые, одинаково распределенные случайные величины с плотностью распределенияf(t). Таким образом, подобрав модель регрессии можно оценить параметр сдвига\Delta. При использовании метода наименьших квадратов МНК-оценкой для\Deltaбудет разность\bar{Y}-\bar{X}. При использовании метода, основанного на рангах с использованием score функции Вилкоксона, оценкой для\Deltaбудет уже рассмотренная выше оценка Ходжеса-Лемана медиана парных разностей.

В следующей R сессии представлены результаты ранговой регрессии для имеющихся данных.

> z = c(x, y)> ci <- c(rep(0, n1), rep(1, n2))> fit <- Rfit::rfit(z ~ ci, scores = Rfit::wscores)> coef(summary(fit))            Estimate Std. Error  t.value      p.value(Intercept)     41.8   4.400895 9.498068 1.960951e-08ci              10.4   5.720594 1.817993 8.574801e-02

Таблица с результатами, наряду с самой оценкой10.4содержит её стандартную ошибку5.72. Используя это, можно, например, построить приблизительный95\%доверительный интервал для сдвига\Delta, основанный на квантиле уровня1-0.05/2 t-распределения сn-2степенями свободы:

> conf.level <- 0.95> estse <- coef(summary(fit))[2, 1:2]> alpha <- 1 - conf.level> alternative = "two.sided"> tcvs <- switch(+   alternative,+   two.sided = qt(1 - alpha / 2, n - 2) * c(-1, 1),+   less = c(-Inf, qt(1 - alpha, n - 2)),+   greater = c(qt(alpha, n - 2), Inf)+ )> conf.int <- estse[1] + tcvs * estse[2]> cat(100 * conf.level, " percent confidence interval:\n", conf.int)95  percent confidence interval: -1.618522 22.41852 

Построенные доверительный интервал(-1.62,22.42)несколько шире своего дискретного аналога (-1,18.4) , найденного ранее.

Мы рассмотрели два подхода (процедура Манна-Уитни-Уилкоксона и метод score функций) для проверки гипотезы о наличии сдвига у параметров положения двух генеральных совокупностей. Для величины сдвига построены точечная оценка и два типа доверительных интервала.

Подробнее..

Статистически устойчивый анализ данных тест Манна-Уитни-Уилкоксона и Score функции

21.03.2021 06:04:56 | Автор: admin

В настоящей статье развиваются идеи и распространяются методы, изложенные в прошлой публикации Статистически устойчивый анализ данных: тест Уилкоксона на случай двух выборок. Это простая, но широко используемая на практике модель, так как даже в более сложных ситуациях целевые показатели часто сопоставляются на двух уровнях.

Анализ модели о сдвиге параметров положения двух генеральных совокупностей начинается с описания свободной от распределения ранговой процедуры Манна-Уитни-Уилкоксона (Mann-Whitney-Wilcoxon, MWW), здесь строятся точечные и интервальные оценки для величины сдвига. Далее кратко описывается метод анализа, основанный на применении score функций и, с его помощью, также проверяется нулевая гипотеза о величине параметре сдвига. В заключение, модель для параметра положения формулируется в виде регрессионной задачи, решение которой также позволяет построить точечную и интервальную оценки для параметра сдвига.

Все изложенные в статье методы проиллюстрированы на сквозном примере, реализованном в виде алгоритмов на языке R.

1.ПустьXиY две непрерывные случайные величины:F(t)иf(t)обозначают функцию (cdf) и плотность (pdf) распределения случайной величиныX, аG(t)иg(t)обозначают соответственно функцию (cdf) и плотность (pdf) случайной величиныY. Будем говорить, чтоXи Y следуют модели для параметра положения (location model), если для некоторого параметра\Delta, -\infty<\Delta<\infty имеем

G(t)=F(t-\Delta),~~~~g(t)=f(t-\Delta).

Параметр\Delta это сдвиг параметра положения случайных величинYиX, например, это может быть разница между медианами или средними (в случае, если средние существуют). Отметим, что предложенная модель предполагает равенство параметров масштаба случайных величинXиY.

2.Рассмотрим две независимые друг от друга выборки, извлеченные в соответствии с выбранной моделью. ПустьX_1,\ldots,X_{n_1} случайная выборка из генеральной совокупности с законом распределенияX(с функциями cdf и pdfF(t)иf(t)соответственно), иY_1,\ldots,Y_{n_2} случайная выборка из генеральной совокупности с законом распределенияY(с функциями cdf и pdfF(t-\Delta)иf(t-\Delta)соответственно). Пусть такжеn=n_1+n_2 размер объединённой выборкиX_1,\ldots,X_{n_1},Y_1,\ldots,Y_{n_2}. Рассмотрим гипотезу

H_0:\Delta=0,~~~H_a:\Delta\neq0.

При этом односторонние альтернативы также допустимы.

Далее изложены методы проверки (точный и асимптотический) данной гипотезы, а также обсуждаются подходы для получения оценки параметра сдвига\Delta: точечной оценки и доверительного интервала.

3.Упорядочим элементы выборки по возрастанию, то есть запишем её в виде вариационного ряда. Каждому элементу ряда поставим в соответствие его номер в ряду ранг. Если несколько элементов ряда совпадают по величине, то каждому из них присваивается ранг, равный среднему арифметическому их номеров. Последний элемент в ранжированной выборке изnэлементов должен иметь рангn. Этот факт можно использовать при проверке правильности ранжирования.

> z <- c(12, 18, 11, 5, 11, 5, 11, 11)> rank(z)[1] 7.0 8.0 4.5 1.5 4.5 1.5 4.5 4.5

ПустьR(Y_i)означает рангY_iсреди элементов объединенной выборки, то есть средиX_1,\ldots,X_{n_1},Y_1,\ldots,Y_{n_2}. Тогда статистика теста Уилкоксона имеет вид

T=\sum_{i=1}^{n_2}R(Y_i)

На практике часто используется эквивалентнаяTследующая статистика Манна-Уитни-Уилкоксона (Mann-Whitney-Wilcoxon, MWW). Рассмотрим набор из всехn_1\cdot n_2разностей вида\left\{Y_j-X_i\right\}и пустьT^+ число положительных таких разностей, то есть

T^+=\#_{i,j}\{Y_j-X_i>0\}.

В этом случае, верно равенство

T^+=T-\frac{n_2(n_2+1)}{2}.

Очевидно, что исследуемую двустороннюю гипотезуH_0следует отвергнуть при малых и больших значениях статистикиT. При условииH_0:\Delta=0обе выборки получены из одной и той же генеральной совокупности, поэтому любые наборы рангов равновероятны (например, вероятность того, что подмножество ранговn_2относится к случайной величинеYравна1/C_n^{n_2}). Следовательно, распределение статистикиTпри нулевой гипотезеH_0не зависит (свободно) от распределения генеральной совокупности и точное значение p-value рассчитывается на основе распределенияT(точном распределении рангов).

4.Оценкой параметра сдвига\Deltaв тесте Манна-Уитни-Уилкоксона является медиана выборки, составленной из всехN_d=n_1\cdot n_2парных разностей вида (оценка Ходжеса-Лемана (Hodges-Lehmann))

\hat{\Delta}_W=\mbox{med}_{i,j}\{Y_j-X_i\}.

ПустьD_{(1)}<D_{(2)}<\cdots<D_{(N_d)}вариационный ряд для таких разностей. Если доверительная вероятность равна1-\alphaиc квантиль уровня\alpha/2распределенияT^+, то есть \alpha/2=P_{H_0}[T^+\leq c] , то интервал\left(D_{(c+1)},D_{(N_d-c)}\right)является(1-\alpha)100\%доверительным интервалом для\Delta. Асимптотическим значение дляcявляется величина

c=\frac{n_1n_2-1}{2}-z_{\alpha/2}\sqrt{\frac{n_1n_2(n+1)}{12},}

которая округляется до ближайшего целого и в общем случае довольно близка к фактическому значению.

5.Для примера применим тест Манна-Уитни-Уилкоксона к двум выборкам из t-распределения c5степенями свободы и значением параметра сдвига \Delta=8 .

> x <- round(rt(11, 5) * 10 + 42, 1)> y <- round(rt(9, 5) * 10 + 50, 1)> x [1] 76.6 41.0 59.3 34.9 29.1 45.0 42.6 31.1 32.4 52.5 47.9> y [1] 58.3 47.2 40.1 45.8 62.0 58.7 64.8 48.1 49.5> wilcox.test(y, x, exact = TRUE, conf.int = TRUE, conf.level = 0.95)Wilcoxon rank sum exact testdata:  y and xW = 72, p-value = 0.09518alternative hypothesis: true location shift is not equal to 095 percent confidence interval: -1.0 18.4sample estimates:difference in location                   10.4

Здесь:T^+=72с p-value0.09518доверительный интервал(-1,18.4)включает истинное значение\Delta=8, точечная оценка равна\hat{\Delta}_W=10.4, доверительная вероятность95\%. По умолчанию p-value основано на точном распределении статистикиT^+для малых выборок (n<50) без хвостов. В случае выбора аргумента exact = FALSE и correct = FALSE (без корректировки на непрерывность) используется асимптотический метод, изложенный далее. В этом случае значение p-value равно0.08738.

> wilcox.test(y, x, exact = FALSE, correct = FALSE)Wilcoxon rank sum testdata:  y and xW = 72, p-value = 0.08738alternative hypothesis: true location shift is not equal to 0

6.Рассмотрим функциюa_\varphi(i)=\varphi(i/(n+1)), где функция\varphi(u)(score функция) определена на интервале(0,1)и

\int_0^1\varphi(u)du=0,~~~\int_0^1\varphi^2(u)du=1.

Например,\varphi_{Ns}(u)=\Phi^{-1}(u), где\Phi^{-1}(u) функция, обратная к функции cdf стандартной нормальной случайной величиныN(0,1). В этом случае преобразование a_{Ns} посредством функции\varphi_{Ns}(Normal score function) ставит в соответствие рангам элементов выборки значения, которые ожидаемы тем, как если бы данные были получены из нормального распределения. Заметим, что в данном определении термин normal score употребляется в смысле rankit, а не в смысле standard score или z-score. Наряду с функцией normal score, возможны другие виды score функций, например\varphi_W(u)=\sqrt{12}[u-(1/2)]задаёт score функцию Уилкоксона.

Для выбранной модели для параметра положения определим следующую функцию переменной\Delta:

S_\varphi(\Delta)=\sum_{j=1}^{n_2}a\varphi[R(Y_j-\Delta)],

гдеa_\varphi некоторая score функция,R(Y_j-\Delta) рангиY_j-\DeltaсредиX_1,\ldots,X_{n_1}и Y_1-\Delta,\ldots,Y_{n_2}-\Delta . В этом случае, статистикуS_\varphi=S_\varphi(0)можно использовать для проверки интересующей нас гипотезы:

H_0:\Delta=0,~~~H_a:\Delta>0.

При условии нулевой гипотезыH_0, случайные величиныXиYраспределены одинаково, отсюда следует, что распределение статистикиS_\varphiнезависимо (свободно) от общего распределения генеральной совокупности.

Хотя точное распределениеS_\varphiи может быть получено численно, обычно используют асимптотическое распределение. В условиях нулевой гипотезыH_0распределениеS_\varphi(0)асимптотически нормально, с математическим ожиданием и дисперсией соответственно:

E_{H_0}[S\varphi(0)]=0,~~~\sigma^2_\varphi=Var_{H_0}[S_\varphi(0)]=\frac{n_1n_2}{n(n-1)}\sum_{i=1}^na_\varphi^2(i).

Это позволяет использовать стандартизованную статистикуz_\varphi=S_\varphi(0)/\sigma_\varphiчтобы отвергнуть гипотезуH_0на уровне значимости\alpha, еслиz_\varphi\geq z_\alpha, гдеz_\alpha квантиль уровня(1-\alpha)стандартного нормального распределения. Двусторонняя и левосторонняя гипотезы проверяются аналогично.

7.В следующей R сессии для данных из рассматриваемого в статье примера представлен расчёт асимптотической статистикиz_\varphiи соответствующее ей значение p-value с использованием score функции Уилкоксона (пакет Rfit).

> x <- c(76.6, 41.0, 59.3, 34.9, 29.1, 45.0, 42.6, 31.1, 32.4, 52.5, 47.9)> y <- c(58.3, 47.2, 40.1, 45.8, 62.0, 58.7, 64.8, 48.1, 49.5)> # Объединим выборки x и y>   z = c(x, y)>   n1 = length(x)>   n2 = length(y)>   n = n1 + n2> # Выберем в качестве score функции функцию Уилкоксона>   scores = Rfit::wscores > # Найдём соответствующие score значения для рангов выборки z >   rs = rank(z)/(n + 1)>   asg = Rfit::getScores(scores, rs)> # Найдём значение статистики Sphi при нулевой гипотезе  >   Sphi = sum(asg[(n1 + 1):n])> # Найдём дисперсию Sphi>   asc = Rfit::getScores(scores, 1:n/(n + 1))>   varphi = ((n1 * n2)/(n * (n - 1))) * sum(asc^2)> # Найдём значения zphi и p-value >   zphi = Sphi/sqrt(varphi)>   alternative = "two.sided">   pvalue <-+     switch(+     alternative,+     two.sided = 2 * (1 - pnorm(abs(zphi))),+     less = pnorm(zphi),+     greater = 1 - pnorm(zphi)+   )> # Выведем результаты>   res <- list(Sphi = Sphi, statistic = zphi, p.value = pvalue)>   with(res, cat("statistic = ", statistic, ", p-value = ", p.value, "\n"))statistic =  1.709409 , p-value =  0.08737528

Таким образом, результаты точногоT^+=72с p-value 0.0952 и асимптотическогоz_W=1.71с p-value0.0874анализа крайне близки: нулевая гипотеза не отвергается на уровне значимости5\%и существует значимая разница в сдвиге параметра положения при уровне значимости10\%.

8.Cформулируем двухвыборочную задачу для параметра положения в виде регрессионной задачи. Пусть\bar{Z}=(X_1,\ldots,X_{n_1},Y_1,\ldots,Y_{n_2})^T,\bar{c} этоn\times1вектор сi-ым элементом0для1\leq i\leq n_1и1дляn_1+1\leq i\leq n=n_1+n_2. Тогда модель для параметра положения можно записать в виде

Z_i=\alpha+c_i\Delta+e_i,

гдеe_1,\ldots,e_n независимые, одинаково распределенные случайные величины с плотностью распределенияf(t). Таким образом, подобрав модель регрессии можно оценить параметр сдвига\Delta. При использовании метода наименьших квадратов МНК-оценкой для\Deltaбудет разность\bar{Y}-\bar{X}. При использовании метода, основанного на рангах с использованием score функции Вилкоксона, оценкой для\Deltaбудет уже рассмотренная выше оценка Ходжеса-Лемана медиана парных разностей.

В следующей R сессии представлены результаты ранговой регрессии для имеющихся данных.

> z = c(x, y)> ci <- c(rep(0, n1), rep(1, n2))> fit <- Rfit::rfit(z ~ ci, scores = Rfit::wscores)> coef(summary(fit))            Estimate Std. Error  t.value      p.value(Intercept)     41.8   4.400895 9.498068 1.960951e-08ci              10.4   5.720594 1.817993 8.574801e-02

Таблица с результатами, наряду с самой оценкой10.4содержит её стандартную ошибку5.72. Используя это, можно, например, построить приблизительный95\%доверительный интервал для сдвига\Delta, основанный на квантиле уровня1-0.05/2 t-распределения сn-2степенями свободы:

> conf.level <- 0.95> estse <- coef(summary(fit))[2, 1:2]> alpha <- 1 - conf.level> alternative = "two.sided"> tcvs <- switch(+   alternative,+   two.sided = qt(1 - alpha / 2, n - 2) * c(-1, 1),+   less = c(-Inf, qt(1 - alpha, n - 2)),+   greater = c(qt(alpha, n - 2), Inf)+ )> conf.int <- estse[1] + tcvs * estse[2]> cat(100 * conf.level, " percent confidence interval:\n", conf.int)95  percent confidence interval: -1.618522 22.41852 

Построенные доверительный интервал(-1.62,22.42)несколько шире своего дискретного аналога (-1,18.4) , найденного ранее.

Мы рассмотрели два подхода (процедура Манна-Уитни-Уилкоксона и метод score функций) для проверки гипотезы о наличии сдвига у параметров положения двух генеральных совокупностей. Для величины сдвига построены точечная оценка и два типа доверительных интервала.

Подробнее..

Статистически устойчивый анализ данных. Тест Флигнера-Киллина

31.03.2021 04:09:55 | Автор: admin

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

ПустьX_1,\ldots,X_{n_1} случайная выборка объёмаn_1с плотностью распределенияf[(x-\theta_1)/\sigma_1]иY_1,\ldots,Y_{n_2} случайная выборка объёмаn_2с плотностью распределенияf[(y-\theta_2)/\sigma_2], гдеf(x) симметричная функция плотности распределения случайной величины, \sigma_1,\sigma_2>0 .

Отметим, что в оговоренных условиях, имеем

\frac{X-\theta_1}{\sigma_1}=\frac{Y-\theta_2}{\sigma_2},

откуда, в частности, следует

\Delta=\ln|X-\theta_1|-\ln|Y-\theta_2|=\ln\left(\frac{\sigma_1}{\sigma_2}\right)=\ln\eta.

2.Интересующая нас гипотеза имеет вид

H_0:\eta=1,~~~H_a:\eta\neq1,

где\eta=\sigma_1/\sigma_2. Далее рассматривается, основанная на рангах, процедура для проверки указанной гипотезы тест Флигнера-Киллина (Fligner-Killeen test), а также строятся точечная оценка и доверительный интервал для параметра\eta.

3.Так как тест для проверки гипотезыH_0должен не зависеть от параметров положения, то следует центрировать имеющиеся наблюдения. В параметрическом F-тесте (var.test {stats}), основанном на отношении выборочных дисперсий, для этой цели используются выборочные средние; вместо них мы применим, равные им в силу симметрииf(x), медианы:

\begin{array}{ll}X_i^*=X_i-\mbox{med}\{X_i\},&i=1,\ldots,n_1,\\Y_j^*=Y_j-\mbox{med}\{Y_j\},&j=1,\ldots,n_2.\end{array}

Исключив из дальнейшего рассмотрения возможные нулевые значения, найдём логарифмы абсолютных величин центрированных наблюдений. В результате получим две выборки\ln|X_1^*|,\ldots,\ln|X_{n_1}^*|и\ln|Y_1^*|,\ldots,\ln|Y_{n_2}^*|, разница в параметрах положения которых (ключевой момент!) равна\Delta=\ln\eta.

Таким образом, задача о различии параметров масштаба сведена к задаче о различии параметров положения. Применим для её решения анализ, основанный на score функциях, разбор которого начат в предшествующей статье Статистически устойчивый анализ данных: тест Манна-Уитни-Уилкоксона и Score-функции.

4.Так как логарифмическая функция монотонно возрастает, то ранги исходных наблюдений и ранги их логарифмов совпадают. Это позволяет сформировать следующую статистику

S_\varphi(0)=\sum_{j=1}^{n_2}a_\varphi[R(\ln|Y_j^*|)]=\sum_{j=1}^{n_2}a_\varphi[R(|Y_j^*|)],

где ранги рассчитываются по объединенной выборке. Данная статистика может быть использована для проверки исследуемой нулевой гипотезыH_0о величине параметра\eta[см. пункты 6, 7 прошлой статьи].

Напомним, что в условиях нулевой гипотезы распределение статистикиS_\varphi(0)асимптотически нормально, с нулевым математическим ожиданиемE_{H_0}[S_\varphi(0)]=0и дисперсией

\sigma_\varphi^2=Var_{H_0}[S_\varphi(0)]=\frac{n_1n_2}{n(n-1)}\sum_{i=1}^na_\varphi^2(i).

Это позволяет использовать стандартизованную статистику

z_\varphi=\frac{S_\varphi(0)}{\sigma_\varphi}

чтобы отвергнуть гипотезуH_0на уровне значимости\alpha, еслиz_\varphi\geq z_\alpha, гдеz_\alpha квантиль уровня(1-\alpha)стандартного нормального распределения.

Решение вопроса о том, какую score функцию использовать при расчете статистикиS_\varphi(0)зависит от вида функцииf(x). В распространенном случае, когдаf(x)имеет нормальное распределение, оптимальной является квадратичная нормальная score функция (squared-normal score) вида

\varphi_{FK}=\left(\Phi^{-1}\left(\frac{u+1}{2}\right)\right)^2-1,

где\Phi cdf стандартного нормального распределения, FK аббревиатура, соответствующая работе Fligner и Killeen (1976) [1], где данная функция была предложена.

5.Для демонстрации изложенной выше методики рассмотрим данные конкретного эксперимента (Doksum and Sievers (1976) [2]) о влиянии озона на прирост в весе у крыс. Контрольная группа крысx(n_1=23)в течение7дней находилась в обычной среде, в то время как экспериментальная группа крысy(n_2=22)в течение 7 дней находилась в среде с повышенной концентрацией озона. В конце недели изменение в весе каждой крысы были взяты в качестве отклика.

Ящик-с-усами исходных данных.Ящик-с-усами исходных данных.

Следующая программа на зыке R содержит алгоритм расчета статистикиz_{FK}и соответствующего значения p-value для проверки двухсторонней гипотезы о различии параметров масштаба двух выборок.

> x <-+   c(+     41.0,    38.4,    24.4,    25.9,    21.9,+     18.3,    13.1,    27.3,    28.5,   -16.9,+     26.0,    17.4,    21.8,    15.4,    27.4,+     19.2,    22.4,    17.7,    26.0,    29.4,+     21.4,    26.6,    22.7)> y <-+   c(+     10.1,     6.1,    20.4,     7.3,    14.3,+     15.5,    -9.9,     6.8,    28.2,    17.9,+     -9.0,  - 12.9,    14.0,     6.6,    12.1,+     15.7,    39.9,   -15.9,    54.6,   -14.7,+     44.1,    -9.0)> > # Зададим squared-normal score функцию>   fkscores = new(+     "scores",+     phi = function(u) {+       (qnorm((u + 1) / 2)) ^ 2 - 1+     },+     Dphi = function(u) {+       qnorm((u + 1) / 2) / dnorm(qnorm((u + 1) / 2))+     }+   )> # Проверим гипотезу о равенстве 0 параметра Delta>   zed = c(abs(x - median(x)), abs(y - median(y)))>   n1 = length(x)>   n2 = length(y)>   n = n1 + n2> > # В расчётe используется стандартизированная score функция>   rz = rank(zed)/(n + 1)>   afk = Rfit::getScores(fkscores, rz)> > # Расчет статистики Sfk, zfk, p-value>   Sfk = sum(afk[(n1+1):n])> >   varfk = ((n1 * n2)/(n*(n-1))) * sum(afk^2)>   zfk = Sfk/sqrt(varfk)>   see = "two.sided">   if (see == "two.sided") {+     pvalue = 2 * (1 - pnorm(abs(zfk)))+   }>   if (see == "greater") {+     pvalue = 1 - pnorm(zfk)+   }>   if (see == "less") {+     pvalue = pnorm(zfk)+   }> > # Вывод результатов>   res <- list(statistic = zfk, p.value = pvalue)>   with(res, cat("statistic = ", statistic, ", p-value = ", p.value, "\n"))statistic =  2.095976 , p-value =  0.03608434

Стандартизованная статистика теста Флигнера-Киллина имеет значениеz_{FK}=2.095c p-value0.036, то есть можно сказать, что наличие озона влияет на вариабельность в приросте веса у крыс.

6.Для того чтобы получить точечную оценку и доверительный интервал для параметра\eta сформулируем нашу задачу в виде лог-модели. Пусть

Z_i=\left\{\begin{array}{ll}\ln|X_i^*|&i=1,\ldots,n_1,\\\ln|Y_{i-n_1}^*|&i=n_1+1,\ldots,n_1+n_2.\end{array}\right.

\bar{c} вектор-индикатор, с первымиn_1элементами равными нулю и последнимиn_2элементами равными единице. Тогда регрессионная задача [см. пункт 8 прошлой статьи] имеет вид

Z_i=\Delta c_i+e_i,~~~i=1,2,\ldots,n.

Применяя алгоритм построения ранговой регрессии (rfit {Rfit}) с использованием квадратичной нормальной score функций\varphi_{FK}можно найти оценку\hat{\Delta}_{FK}параметра\Delta. Откуда оценка для параметра\etaполучается в виде\hat{\eta}_{FK}=\exp\{\hat{\Delta}_{FK}\}.

Далее, используя найденную в ходе построения регрессии величину стандартной ошибки\mbox{SE}(\hat{\Delta}_{FK}), можно построить, например, приблизительный95\%доверительный интервал для\Delta, основанный на квантиле уровня1-\alpha/2t-распределения сn'-2степенями свободы

L_{FK}=\hat{\Delta}_{FK}-t_{\alpha/2,n'-2}\cdot\mbox{SE}(\hat{\Delta}_{FK}),~~~U_{FK}=\hat{\Delta}_{FK}+t_{\alpha/2,n'-2}\cdot\mbox{SE}(\hat{\Delta}_{FK}),

гдеn' объем выборкиZ_i,0<\alpha<1. Рассчитанные границы также преобразуются в соответствующие границы доверительного интервала для\eta:(\exp\{L_{FK}\},\exp\{U_{FK}\}). Отметим, что найденные таким образом доверительные границы всегда положительны.

7.Следующая программа на языке R содержит алгоритм расчета точечной оценки и доверительного интервала для параметра\eta.

> # Подготовим данные   >   xs = abs(x - median(x))>   xs = xs[xs != 0]>   xstarl = log(xs)>   ys = abs(y - median(y))>   ys = ys[ys != 0]>   ystarl = log(ys)>   n1s = length(xs)>   n2s = length(ys)>   ns = n1s + n2s> > # Построим уравнение регрессии>   cvec = c(rep(0, n1s), rep(1, n2s))>   zed = c(xstarl, ystarl)>   fitz = rfit(zed ~ cvec, scores = fkscores)>   (sumf = summary(fitz))Call:rfit.default(formula = zed ~ cvec, scores = fkscores)Coefficients:            Estimate Std. Error t.value   p.value    (Intercept)  1.42288    0.38905  3.6573 0.0007044 ***cvec         0.86586    0.42783  2.0238 0.0493802 *  ---Signif. codes:  0 *** 0.001 ** 0.01 * 0.05 . 0.1   1Multiple R-squared (Robust): 0.07815811 Reduction in Dispersion Test: 3.56096 p-value: 0.06608 >   delta = coef(sumf)[2, 1]> > # Оценка отношения параметров масштаба>   (eta = exp(delta))[1] 2.377049> > # Построим доверительный интервал>   conf.level = 0.95>   se = coef(sumf)[2, 2]>   tc = abs(qt(((1 - conf.level)/2), ns - 2))>   lb = delta - tc * se>   ub = delta + tc * se>   conf.int = c(exp(lb), exp(ub))>   cat(100 * conf.level, " percent confidence interval:\n", conf.int)95  percent confidence interval: 1.002462 5.636488

Таким образом, ранговая оценка параметра\etaравна2.337c95\%доверительным интервалом(1.002,5.636). Это также подтверждает вывод о том, что нулевую гипотезу H_0 о равенстве параметров масштаба следует отвергнуть.

8.В заключение отметим, что рассматриваемая в статье ранговая процедура на основе предложенной Флигнером и Киллингом квадратичной нормальной score функции позволяет успешно проверять гипотезы о различии параметров масштаба, в двух важных случаях:

  • когда плотность распределение функции ошибкиf(x)симметрична, то есть процедура обобщает чувствительный к отклонениям от нормальности F-критерий;

  • когда распределение генеральной совокупности относится к классу засорённых нормальных распределений (skewed contaminated normal distributions). В этом случае случайная величина получается по законуX=X_1\cdot(1-I_\epsilon)+I_\epsilon\cdot X_2, гдеX_1иX_2имеютN(0,1)и N(\mu_c,\sigma_c^2) распределения соответственно,I_\epsilon имеет распределение Бернулли с вероятностью успеха\epsilonиX_1,X_2иI_\epsilonнезависимы в совокупности.

Ссылки на литературу:

  1. Fligner, M. A. and Killeen, T. J. (1976), Distribution-free two-sample test for scale, Journal of the American Statistical Association, 71, 210-213.

  2. Doksum, K. A. and Sievers, G. L. (1976), Plotting with confidence: Graphical comparisons of two populations, Biometrika, 63, 421-434.

Подробнее..

Запросить 100 серверов нельзя оптимизировать код. Ставим запятую

15.06.2021 20:16:32 | Автор: admin

Можно выделить ряд алгоритмов, которые являются базовыми и лежат в основе практически каждой строчки программ, написанных на языках высокого уровня. Хорошо иметь под руками классический многотомный труд Дональда Кнута "The Art of Computer Programming", там детально разобраны многие базовые алгоритмы. Но прочесть и усвоить все задача, требующая много усилий и времени, которая должна как-то быть мотивирована.


Многие могут предположить, что нюансы необходимо было знать 50 лет назад, а сейчас можно пользоваться готовыми пакетами и функциями и не погружаться в детали. Однако, это далеко не так. Равно как никто не отменял важность понимания представления методов хранения данных в памяти и их обработки в процессоре.


Далее разберем нюансы на примере функций сортировки. Сортировка и поиск используются максимально часто во всех манипуляциях с данными. Экономия нескольких миллисекунд на операции может приводить к суммарному сокращению часов расчета на значительных данных.


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


Введение


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


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


  • case_id уникальный идентификатор кейса/прецедента;
  • record журнальная запись события в кейсе;
  • start время регистрации.

Используемые библиотеки


library(tidyverse)library(data.table)library(rTRNG)

Наиболее интересной задачей является генерация временных меток. События должны идти последовательно во времени для каждого кейса в отдельности. Сначала подготовим простую "рыбу". В частном случае мы возьмем для демонстрации малое число кейсов. В продуктиве их может быть 10^5-10^n, что определяется задачами.


Пример кода
# определим число кейсовnn <- 100# создаем первичный набор кейсовrecords <- c("first", "one and a half", "second", "third", "fourth",              "fifth", "sixth")# готовим два варианта для экспериментовdf <- tibble(case_id = 1:nn, recs = list(records)) %>%  unnest(recs)dt <- as.data.table(df)[, case_id := as.numeric(case_id)]# указание ключа приводит к физической сортировке данныхsetkey(dt, case_id)head(df, 10)

  # A tibble: 10 x 2     case_id recs                 <int> <chr>            1       1 first            2       1 one and a half   3       1 second           4       1 third            5       1 fourth           6       1 fifth            7       1 sixth            8       2 first            9       2 one and a half  10       2 second  

Теперь приступим к интересному блоку генерации временных меток. Для простоты задачи сведем ее к распределению долей в интервале [0; 1] в рамках каждого кейса. Перевод в реальный unixtimestamp оставим за пределами, это неинтересно. Варианты с явными циклами также за пределами. Времена исполнения приведены на условном компьютере, главное, что выполняется все на одном.


Создание одной временнОй метки


Вариант 1. Прямолинейный


Этот вариант предлагается в большинстве случаев. А что, все просто и понятно.


Пример кода
f1 <- function(df) {  df %>%    group_by(case_id) %>%    mutate(t_idx = sort(runif(n(), 0, 1))) %>%    ungroup()}

Получаем такие условные показатели. Наверное, неплохо. Но не забываем, что тут всего 100 кейсов.


  median `itr/sec` mem_alloc 15.38ms      63.2   284.9KB

Подумаем, что можно улучшить?


Вариант 1+1/2. Прямолинейный + быстрый генератор чисел


Есть хорошая библиотека rTRNG. На больших объемах она дает существенное ускорение, в том числе, за счет параллельной генерации. Просто проверим:


Пример кода
f1_5 <- function(df) {  df %>%    group_by(case_id) %>%    mutate(t_idx = sort(runif_trng(n(), 0, 1))) %>%    ungroup()}

  median `itr/sec` mem_alloc 29.34ms      29.5   284.9KB

На малых объемах не получили никакого выигрыша. Это все? Конечно же нет. Мы знаем, что tidyverse медленнее data.table, попробуем применить его. Но здесь мы попробуем применить первую хитрость отсортировать вектор времен по индексам, а потом его переприсвоить.


Вариант 2. Однопроходный, через индексы data.table


Пример кода
f2 <- function(dt) {  # здесь полагаемся на то, что мы заранее отсортировали уже по `case_id``  # формируем случайные числа и сортируем их по кейсам  vec <- dt[, t_idx := runif_trng(.N, 0, 1)][order(case_id, t_idx), t_idx]  # возвращаем сортированный   dt[, t_idx := vec]}

Получается вполне неплохо, ускорение раз в 15-20 и памяти требуется почти в три раза меньше.


  median `itr/sec` mem_alloc   1.69ms     554.      109KB 

Останавливаемся? А почему да?


Вариант 3. Однопроходный, через композитный индекс


На самом деле, как только мы сваливаемся в цикл, явный, или через by, мы резко просаживаемся в производительности. Попробуем сделать все за один проход. Идея следующая сделать композитный индекс, который позволил бы нам отсортировать все события одним махом. Используем трюк. Поскольку у нас внутри кейса все временные метки будут в диапазоне [0; 1], то мы можем разделить индекс на две части. Целочисленная часть будет содержать case_id, дробная часть временнУю долю. Однократная сортировка одного такого индекса сохранит принадлежность строчек case_id, при этом мы одним махом отсортируем значения внутри каждого кейса


Пример кода
f3 <- function(dt) {  # делаем трюк, формируем композитный индекс из case_id, который является монотонным, и смещением по времени  # поскольку случайные числа генерятся в диапазоне [0, 1], мы их утаскиваем в дробную часть (за запятую)  # сначала просто генерируем случайные числа от 0 до 1 для каждой записи отдельно   # и масштабируем одним вектором  dt[, t_idx := sort(case_id + runif_trng(.N, 0, 1, parallelGrain = 10000L)) - case_id]}

Запускаем и получаем выигрыш еще в 2 раза против предыдущего варианта, как по времени, так и по памяти.


  median `itr/sec` mem_alloc 826.7us    1013.     54.3KB

Вариант 3+1/2. Однопроходный, через композитный индекс, используем set


Останавливаемся? Можно и остановиться, хотя поле для сжатия еще есть. Дело в том, что при таких малых временах исполнения накладные расходы на NSE становятся весьма ощутимыми. Если использовать прямые функции, то можно получить куда лучший результат.


Пример кода
f3_5 <- function(dt) {  set(dt, j = "t_idx",       value = sort(dt$case_id + runif(nrow(dt), 0, 1)) - dt$case_id)}

Ускорение еще в 5 раз, памяти потребляем в 4 раза меньше


  median `itr/sec` mem_alloc 161.5us    5519.     16.3KB

Промежуточное подведение итогов


Соберем все вместе.


Тестируем
bench::mark(  f1(df),  f1_5(df),  f2(dt),  f3(dt),  f3_5(dt),  check = FALSE)

  expression      min   median `itr/sec` mem_alloc  <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>1 f1(df)       14.3ms  15.38ms      63.2   284.9KB2 f1_5(df)    24.43ms  29.34ms      29.5   284.9KB3 f2(dt)       1.55ms   1.69ms     554.      109KB4 f3(dt)        722us  826.7us    1013.     54.3KB5 f3_5(dt)    142.5us  161.5us    5519.     16.3KB

Путем изучения принципов работы алгоритмов и пристального взгляда на задачу первичный наивный вариант удалось бесплатно улучшить по производительности в 90 раз, а потребление памяти сократить в 18 раз. Это показатели, которые слабо достижимы горизонтальным масштабированием серверов.


Создание временнОй метки начала записи и окончания


Усложним немного задачу. Теперь для каждой записи надо сформировать время начала и время окончания ("прием у врача"). При этом все записи должны соблюдать исходный порядок следования во времени и время окончания для каждой записи должно быть не раньше времени начала.


Количество рассматриваемых сценариев сократим, оставим только наиболее интересные. И всякие варианты с явными циклами тоже даже не упоминаем.


Вариант 1. Прямолинейный


Первый наиболее разумный и очевидный вариант создать метки начала, как в предыдущей задаче и потом для каждой метки поставить случайное окончание, так, чтобы соблюдались правила. Итерируем построчно, есть специфика, что долевое начало следующего кейса будет, как правило, меньше, чем начало последней операции предыдущего. Но это тоже решаемо. Код может выглядеть примерно так:


Пример кода
# Cоздание ЧЕТРЕХ колонок -- case_id, record, start, finish# Все как в предыдущем, только для каждого записи finish > start # и для двух последовательных записей 1, 2 в одном кейсе start_2 > finish_1 dt[, t_idx := NULL] # очистим хвосты предыдущего упражненияf1 <- function(df) {  df %>%    group_by(case_id) %>%    mutate(ts_idx = sort(runif(n(), 0, 1))) %>%    ungroup() %>%    # еще раз пройдемся генератором, используя время начала следующей записи как границу    # чтобы избежать NaN при переходе между кейсами (в случае max < min),     # принудительно выставим порог 1 в таких переходах, NA в последней строке тоже заменим на 1    mutate(tf_idx = {lead(ts_idx, default = 1) %>% if_else(. > ts_idx, ., 1)}) %>%    mutate(tf_idx = map2_dbl(ts_idx, tf_idx, ~runif(1, .x, .y)))}

В целом меньше секунды, но, очевидно, что это ОЧЕНЬ далеко от оптимальности.


  median `itr/sec` mem_alloc  28.16ms      30.7    2.06MB 

Вариант 2. Однопроходный, через композитный индекс и матрицы


Идея следующая. Нам нужно получить 2 колонки, в которых значения будут отсортированы строчка за строчкой и значение первой колонки не превышает значение второй колонки для каждой строки. Но это же матрица! Матрица это единый кусок в памяти, который сопровождается атрибутами сегментирования. Работа с ними куда быстрее классических датафреймов.


Пример кода
f2 <- function(dt){  dt[, c("ts_idx", "tf_idx") := {    # используем принцип vector recycling    x <- case_id + runif(2 * .N, 0, 1);    m <- matrix(sort(x), ncol = 2, byrow = TRUE) - case_id;    list(m[, 1], m[, 2])  }]}

В легкую сократили время и память почти в 30 раз! Да и код стал существенно проще и прямолинейнее.


  median `itr/sec` mem_alloc   1.04ms     733.    74.38KB 

Вариант 2+1/2. Однопроходный, через композитный индекс, матрицы и set


Пример кода
f2_5 <- function(dt){  x <- dt$case_id + runif(2 * nrow(dt), 0, 1)  m <- matrix(sort(x), ncol = 2, byrow = TRUE) - dt$case_id  set(dt, j = "ts_idx", value = m[, 1])  set(dt, j = "tf_idx", value = m[, 2])}

Перфекционизм в действии. Еще в 4 раза ускорили.


  median `itr/sec` mem_alloc  278.1us    2781.    57.55KB 

Промежуточное подведение итогов


Соберем все вместе.


Тестируем
bench::mark(  f1(df),  f2(dt),  f2_5(dt),  check = FALSE)

  median `itr/sec` mem_alloc  28.16ms      30.7    2.06MB   1.04ms     733.    74.38KB  278.1us    2781.    57.55KB 

Первичный наивный вариант также удалось бесплатно улучшить по производительности в 90 раз, а потребление памяти сократить в 35 раз.


Заключение


Примеры вполне себе реальные, каждый может поэкспериментировать на своей машине. Абсолютные показатели будут, естественно, отличаться, но тренд останется неизменным. Чтобы убедиться, что большое количество кода в мире пишется по самым наивным вариантам, достаточно пошерстить репозитории на гитхабе. И ведь не всегда значительная оптимизация это громадный непонятный код и уйма времени, см. примеры выше. Оптимизированный код даже компактнее и прозрачнее, устраняются промежуточные пассы.


Поэтому, когда представители BigData запрашивают немалые вычислительные ресурсы и требуемое время для относительно небольшой задачи, всегда терзает смутное сомнение, что под капотом что-то не то.


Предыдущая публикация Оценка структуры кредитного портфеля с помощью R.

Подробнее..

Звездные войны или подробный гайд по dplyr

04.05.2021 18:23:50 | Автор: admin

Сегодня, 4 мая, в день Звездных войн мы подготовили для Вас подробный гайд по основным функциям библиотеки dplyr. Почему именно в день Звездных войн? А потому что разбирать мы все будем на примере датасета starwars.

Ну что, начнем!

Если Вы хотите получать еще больше интересных материалов по программированию, Data Science и математике, то подписывайтесь на нашу группу ВК, канал в Телеграме и Инстаграм. Каждый день мы публикуем полезный контент и вопросы с реальных собеседований.

Кстати говоря, а Вы знаете, почему день Звездных войн отмечается именно 4 мая? Все очень просто - знаменитая фраза May the fource be with you крайне созвучна с May, the 4th, т.е. 4 мая :)

Знакомство с датасетом

Для начала, давайте подключим библиотеку dplyr. Делается это с помощью функции library.

Вместе с этой библиотекой нам становится доступен и датасет starwars. Давайте выведем первые несколько строк на экран и посмотрим, какую информацию он в себе хранит.

  1. name - имя или прозвище героя вселенной Звездных войн. Например, Оби-Ван Кеноби.

  2. height - высота персонажа

  3. mass - масса персонажа

  4. hair_color - цвет волос

  5. skin_color - цвет кожи

  6. eye_color - цвет глаз

  7. birth_year - год рождения (до битвы на Явине)

  8. sex - биологический пол (есть существа без пола и гермафродиты)

  9. gender - поведенческий пол персонажа (например, на какой пол запрограммированы дроиды)

  10. homeworld - из какой вселенной существо

  11. species - биологический вид

  12. films - список фильмов, в которых появилось существо

  13. vehicles - список транспорта, которым существо управляло

  14. starships - список космических кораблей, которыми существо управляло

В принципе, все предельно понятно, поэтому можем переходить к знакомству с dplyr и обработке нашего датасета. Дадим полет нашей фантазии и немного покрутим-повертим наши данные :)

Знакомство с dplyr

dplyr - это один из основных пакетов семейства tidyverse. Его ближайший аналог в Python - библиотека Pandas. Библиотека dplyr служит для манипуляции с данными: фильтрация, сортировка, создание вычислимых столбцов и так далее.

По своему функционалу библиотека dplyr очень похожа на стандартный синтаксис SQL. Немного ранее мы вместе с Алексеем Селезневым из Netpeak делали карточки: сравнение глаголов dplyr и операторов SQL. Вы можете посмотреть их здесь.

Перед тем, как переходить к рассмотрению глаголов библиотеки dplyr, нужно упомянуть, что все пакеты tidyverse основаны на концепции tidy data. Если коротко, то аккуратные данные - это способ организации датафреймов по трем основным постулатам:

  1. Каждая переменная находится в отдельном столбце

  2. Каждое измерение находится в отдельной строке

  3. Каждое значение находится в отдельной ячейке

Кстати говоря, наш датасет starwars не совсем соответствует этим правилам. Нам это не помешает, но сможете ли Вы найти, в чем именно несоответствие? ;)

Если Вы хотите поподробней познакомиться с tidy data, рекомендуем нашу статью.

Итак, вернемся к глаголам dplyr. Пакет dplyr позволяет делать несколько основных операций:

  • Отбор столбцов

  • Фильтрация строк

  • Сортировка строк

  • Группировка

  • Агрегирование

  • Создание вычислимых столбцов

  • Объединение таблиц

Давайте переходить к практике - хватит теории!

Отбор столбцов

Давайте начнем с самого простого - как выбрать только определенные столбцы из таблицы? Очень просто - с помощью функции select.

А что если нам проще указать, какие столбцы не надо отбирать? Например, в таблице 20 столбцов, а нам нужно исключить только первый столбец. Или столбец с определенными названиями. Не проблема - просто добавьте знак минус:

Но и это не все возможности функции select. Мы можем воспользоваться вспомогательными функциями, чтобы кастомизировать отбор столбцов. Вот список этих функций:

  • contains: название столбца содержит

  • ends_with: название столбца заканчивается на

  • matches: название столбца соответствует регулярному выражению

  • num_range: поиск занумерованных столбцов, например, V1, V2, V3...

  • one_of: название столбца соответствует одному из вариантов

  • starts_with: название столбца начинается с

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

Теперь отберем столбцы, название которых является одним из указанных.

И в заключение - поиск по регулярному выражению: отбираем столбцы, в названии которых буква t стоит на любом месте, но после нее обязательно идет хотя бы 1 символ.

Обратите внимание, все наши запросы возвращали таблицу tibble. А что, если мы хотим отобрать столбец и сразу начать работать с ним, как с вектором? Мало кто знает, но для этого в dplyr есть специальная функция pull. Она возвращает не таблицу, как остальные глаголы dplyr, а вектор.

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

Фильтрация строк

Фильтрация строк по значениям - это аналог привычного оператора WHERE в SQL. В синтаксисе dplyr же для этого используется глагол filter (как неожиданно, правда?).

Использовать filter очень просто - задаем некое логическое выражение и функция вернет только те строки, для которых это выражение True. Например:

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

Логические выражения Вы можете конструировать не только с помощью >/<, но и с помощью других логических операторов:

  • >=

  • <=

  • is.na

  • !is.na

  • %in%

  • !

Например:

Помимо функции filter, отбирать строки можно и с помощью других глаголов. Например, функция distinct позволяет исключить дублирующиеся значения в определенном столбце.

Другая функция, sample_n, отбирает n случайных строк.

Функция slice же, например, позволяет отбирать строки по их индексу:

Функция sample_frac делает случайную подвыборку из таблицы. Нужно указать долю строк из общего числа, которые должны быть в итоговой таблице. Например, при параметре 0.5 вернется половина строк из таблицы, выбранные случайным образом.

Итак, с фильтрацией строк мы тоже разобрались, переходим к следующему разделу.

Сортировка строк

За сортировку строк в SQL отвечает оператор ORDER BY. В dplyr для этого существует функция arrange.

Синтаксис максимально прост - достаточно указать столбец, по которому выполняется сортировка.

Чтобы отсортировать строки по убыванию, достаточно добавить функцию desc.

А если отсортировать нужно по нескольким столбцам? Легко, просто указываем их названия :)

Кстати говоря, с arrange Вы можете также использовать вспомогательные глаголы, которые мы обсуждали в блоке с select. Для этого нужно использовать функцию across. Например:

Про сортировку, пожалуй, сказать больше нечего - переходим к группировкам.

Группировка и агрегатные функции

Группировка - базовая операция, которая необходима для расчета различных характеристик - средних значений, медиан, сумм, количества строк в группе и так далее. В SQL для этого используется оператор GROUP BY и агрегатные функции sum, min, max и так далее. В dplyr же все то же самое :) Ну, почти.

Давайте для начала сгруппируем наши строки по полю eye_color:

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

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

Обратите внимание, здесь мы дополнительно воспользовались функцией drop_na из пакета tidyr, чтобы удалить строки, в которых есть пропуски. Сделали мы это, чтобы при расчете наших максимальных/минимальных/других агрегатных значений не вылезали значения NA.

На выходе мы получили 4 группы (во всех остальных были пропуски, по всей видимости) и рассчитанные для них характеристики. Также каждому столбцу присвоено то имя, которое мы дали ему в функции summarise.

Мы с Вами здесь использовали только несколько агрегатных функций, но вообще говоря, их больше:

  • n_distinct - считает количество уникальных элементов в группе

  • last - возвращает последнее значение в группе

  • nth - возвращает n-ое значение из группы

  • quantile - возвращает заданную квантиль

  • IQR - межквартильный размах, inter-quartile range

  • mad - медианное абсолютное отклонение, median absolute deviation

  • sd - стандартное отклонение

  • var - вариация

Хорошо, с базовой группировкой мы разобрались. А что если пойти дальше

Продвинутая группировка

А как Вам такая задача - рассчитать все те же самые характеристики, что и в прошлый раз, но сразу для нескольких столбцов? Например, мы делали для mass, а теперь давайте сделаем для mass и height.

Без проблем - достаточно применить уже известную функцию across.

Видно, что характеристики рассчитались по полю mass и height для каждой группы в отдельности. Если не устраивает название столбцов, которое система дала по умолчанию (столбец_характеристики), то можно менять шаблон с помощью параметра .names.

Итак, на этом закончим раздел группировки и перейдем к вычислимым столбцам.

Вычислимые столбцы

Создание вычислимого столбца - стандартная задача. Например, есть столбец A, столбец B, а мы хотим вывести столбец A/B. Или другой вариант - мы хотим проранжировать строки нашей таблицы. Все это можно сделать с помощью функции mutate.

Для начала базовая вещь - создадим вычислимый столбец отношения веса к росту.

А если нам нужно применить одну и ту же функцию к нескольким столбцам? Без проблем - снова нас выручит across. Например, давайте умножим на 10 все столбцы с численным типом данных. При этом мы не будем создавать новые столбцы - мы модифицируем старые.

Видно, что в столбцах с весом, ростом и возрастом все значения умножились на 10.

Давайте и с текстом немного поработаем - ко всем строковым значениям добавим суффикс _new. Для этого нам понадобится библиотека stringr все из того же семейства tidyverse.

Хорошо видно, что ко всем строковым значениям добавился суффикс _new. Таким же образом можно работать и с датой, со временем и с любым другим типом данных.

Помимо всего прочего, mutate позволяет реализовать аналог оконных функций в SQL. Например, давайте проранжируем строки без разрывов по полю mass с помощью функции dense_rank:

Видно, что в конец таблицы добавился новый столбец rnk с рангами для каждой строки.

Таких функций, на самом деле, масса. Вот некоторые из них:

  • lag

  • lead

  • cumsum

  • dense_rank

  • ntile

  • row_number

  • case_when

  • coalesce

Это, пожалуй, самые популярные и все они на 100% перекликаются с SQL. Приведем несколько примеров.

Ну что, давайте переходить к последнему пункту - к объединению таблиц.

Объединение таблиц

Пожалуй, последняя необходимая для полноценной работы с датафреймами операция - объединение таблиц. Операция объединения в dplyr тесно связана с джоинами в SQL. Вот перечень основных функций:

  • left_join

  • right_join

  • inner_join

  • full_join

Аналогичные глаголы присутствуют и в стандартном SQL. Давайте создадим новую таблицу - возьмем датасет starwars и оставим только те строки, где вид существа - человек. А после попробуем различные виды джоинов.

Обратите внимание, мы воспользовались функцией rename и переименовали поле name. Сделали мы это намеренно, чтобы показать работу аргумента by (аналог ON в SQL) для связи столбцов при джоине.

Делаем inner_join, на выходе получаем только 35 строк, т.к. в таблице df строк именно 35. Аргумент by позволил нам указать, через какие столбцы таблицы связаны между собой.

Если сделать full_join аналогичным образом, то строк в итоговой таблице будет 87, т.к. в таблице starwars их 87.

Внимательный читатель может заметить, что при джоине одни и те же столбцы продублировались из двух таблиц. Т.е. столбец mass, например, в итоговой таблице фигурирует с суффиксом .x и .y. Как от этого избавиться?

1 вариант:

Если название столбцов позволяет, то Вы можете убрать аргумент by из джоина. Тогда соединяться таблицы будут по всем столбцам, у которых совпадает названием. В нашем случае этот вариант подходит - названия одинаковые и в результат добавится только один столбец - new_name.

2 вариант:

В реальной жизни, к сожалению, все не так сказочно, поэтому можно сделать небольшую предварительную обработку и джойнить не всю таблицу, а только нужные столбцы. Например, так:

Помимо различных джоинов, в dplyr есть еще другие функции для соединения таблиц:

  • bind_rows - помещает одну таблицу под другой

  • bind_cols - ставит одну таблицу справа от другой

  • intersect - находит пересекающиеся строки

  • setdiff - разность таблиц, т.е. строки из первой таблицы, которых нет во второй

  • union - возвращает строки, которые есть в любой из таблиц (дубликаты исключаются)

  • union_all - аналогично union, но оставляет дуликаты

Эпилог

Мы с Вами рассмотрели все основные операции библиотеки dplyr и почти все основные функции. Все остальное - практика, практика и еще раз практика. Если у Вас будут вопросы - рады будем помочь и ответить в комментариях :)

Если Вы хотите получать еще больше интересных материалов по программированию, Data Science и математике, то подписывайтесь на нашу группу ВК, канал в Телеграме и Инстаграм. Каждый день мы публикуем полезный контент и вопросы с реальных собеседований.

А, и совсем забыли. May the fource be with you!

P.S. Здесь Вы можете найти официальную шпаргалку по всем функциям dplyr. Все удобно и компактно собрано в одном месте :)

Подробнее..

Extendr вызываем rust из R (и наоборот)

14.06.2021 20:18:14 | Автор: admin

Зачем нужен Rust в R?

Первый вопрос, который должен возникнуть у читателя -- а зачем вообще использовать Rust вместе с R? Ответ довольно прост: Rust -- новый системный язык программирования, спроектированный специально для написания безопасного и легко распараллеливаемого кода. Rust довольно сложен в освоении (в сравнении с другими языками), но при этом предоставляет отличные инструменты для разработки. Rust имеет довольно неплохую ООП систему и очень много заимствует из функциональных языков программирования. Несмотря на дополнительную сложность из-за функциональных/ООП компонентов, Rust позиционируется как zero-cost abstraction язык, так же как и C++.

Из-за своей популярности Rust привлекает разработчиков, которые портируют старые библиотеки и разрабатывают новые крейты. Большинство из этих крейтов можно напрямую использовать в пакетах для R, упрощая жизнь разработчикам на R.

Таким образом, можно привести два основных аргумента в пользу использования Rust: более безопасный и удобочитаемый код и доступ к целому репозиторию Rust-крейтов.

Что нужно, чтобы R код мог вызвать Rust-библиотеку?

На самом деле -- не так уж много. R-пакеты могут содержать директорию src/, в которой находится исходный код на одном из компилируемых языков. С помощью src/Makevars или src/Makevars.win файлов (вариация make) можно контролировать процесс сборки, например, вызвав на одном из шагов cargo (см. пример здесь):

cargo build --release --manifest-path=rustlib/Cargo.toml

При этом Rust -библиотека должна собираться как crate-type = ["staticlib"]. Кроме непосредственной компиляции Rust-кода, нужно предоставить C-обертки к экспортируемым функциям, а так же добавить несколько магических вызовов специальных R-функций, которые объясняют R, какие именно функции и какого типа экспортируются из данной библиотеки (например, вот так).

Основная проблема -- C-обертки и преобразование типов из R SEXP (фактически, специальный указатель) во что-то, совместимое с Rust, учитывая при этом специфику управления памятью в R (все эти ваши PROTECT, UNPORTECT, и т. д.). Как результат -- легко создать примитивный прототип без функционала, практически невозможно написать достаточно большой проект.

Интегрируем R и Rust: три простых шага

Шаг первый: баиндинги для заголовочных файлов R

Взаимодействие с R происходит через специализированный API, доступный обычно ввиде C/ C++ заголовочных файлов (см. $R_HOME\include\). Разумеется, вызывать эти методы можно практически из любого языка, но это неудобно -- загловочные файлы невозможно подключить напрямую к Rust. К счастью, у этой проблемы уже давно есть решение: rust-bindgen (rust-lang/rust-bindgen). bindgen позволяет автоматически генерировать Rust-обертки из заголовочных файлов, и делает это довольно эффективно.

Так появился крейт libR-sys, который предоставляет баиндинги ко всем необходимым внутренним R функциям. Генерация баиндингов -- вещь нетривиальная, bindgen зависит от clangи сложен в конфигурировании, поэтому мы предоставляем pre-computed (заранее сгененрированные) баиндинги для большинства платформ, поддерживающих R. Список включает в себя linux-x64 (созданный с помощью Ubuntu-20.04), win-x86/x64 (с помощью msys2, x86 может иметь проблемы в каких-то пограничных случаях), macOS включая 11 версию (по возможности), x64 и экспериментально arm64 (честно я не знаю, есть ли arm64 сборка R под macOS). Для каждой из упомянутых платформ/архитектур мы стараемся предоставить три версии баиндингов: oldrel, release, и devel, что соответствует "прошлой", "текущей" (сейчас это 4.1.0) и "находящейся в разработке" версиям R.

В качестве альтернативы баиндинги можно сгенерировать непосредственно в системе где компилируется R пакет, при условии что все необходимые зависимости присутствуют (особенная головная боль на Windows). Гипотетически, можно собрать R для неподдерживаемой платформы и прямо на месте сгененрирвоать баиндинги (если Rust поддерживает такую платформу).

Исхоный код проекта досутпен здесь. Крейт находится в стадии поддержки, т. к. основная разработка завершена. Сейчас решается вопрос об автоматизации деплоймента новых баиндингов когда происходит релиз новой версии R.

Шаг второй: автоматизируем преобразование типов и экспорт функций

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

Прежде чем продолжить, я хочу сделать небольшое отступление. Вся идея проекта extendr и, в особенности, имплементация большей части Rust-крейтов, принадлежит Энди Томасону (@andy-thomason). Без его вклада, на мой субъективный взгляд, extendr в том виде, в котором он существует сейчас, был бы невозможен.

Вернемся обратно к коду. Как избавиться от боилерплейта? Легко, надо всего лишь распарсить исходный код Rust. Например, используя syn и подобные крейты. Моей экспертизы недостаточно, чтобы детально описать процесс парсинга и кодогенерации, но для конечного пользователя экспорт Rust функции становится невероятно простым. Во-первых, нужно пометить функции с помощью аттрибута #[extendr]:

#[extendr]fn add_i32(x : i32, y : i32) -> i32 { x + y }#[extendr]fn add_vec(x : &[i32], y : &[i32]) -> Vec<i32> {     x.iter().zip(y.iter()).map(|v| v.0 + v.1).collect()}

Во-вторых, нужно явно объявить экспортируемые функции:

extendr_module! {  mod extendrtest;  fn add_i32;  fn add_vec;}

Мы попытались предусмотреть все возможные случаи экспорта функций, включая экспорт нескольких модулей. Возможно, это не будет работать идеально во всех случаях, но в базовых сценариях проблемы отсутствуют. Несмотря на огромное количество готовых фич, к extendr стоит относится как к WIP.

К сожалению, остается одно небольшое ограничение при интеграции Rust-кода в проект. Дело в том, что если в папке src/ отсутствуют файлы-исходники, то стандартная процедура компиляции R попросут игнорирует все остальное и библиотека не компилируется. Чтобы обойти это, в src/ добавляется единственный файл entrypoint.c, примерно следующего содержания:

void R_init_extendrtest_extendr(void *dll);void R_init_extendrtest(void *dll) {  R_init_extendrtest_extendr(dll);}

Здесь R_init_extendrtest_extendr генерируется автоматически с помощью Rust-крейта, а R_init_extendrtest -- непосредственно вызывается из R. Мы пока что не нашли способа избавиться от этого ограничения.

Некоторых изменений требуют и Makevars-файлы. Вот пример из одного из тестовых проектов:

LIBDIR = ./rust/target/releaseSTATLIB = $(LIBDIR)/libextendrtest.aPKG_LIBS = -L$(LIBDIR) -lextendrtestall: C_clean$(SHLIB): $(STATLIB)$(STATLIB):cargo build --lib --release --manifest-path=./rust/Cargo.tomlC_clean:rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS)clean:rm -Rf $(SHLIB) $(STATLIB) $(OBJECTS) rust/target

Фактически, мы отдельно компилируем Rust-крейт, а потом создаем совместимую с R библиотеку используя Rust-библиотеку и результат компиляции entrypoint.c.

Аналогично выглядит и версия для Windows, с той лишь разницей что на Windows мы поддерживаем иx86, и x64, из-за чего приходится динамически выбирать правильный путь к STATLIB.

extendr выполняет не только кодогенерацию на стороне Rust, он еще генерирует обертки на стороне R. Если предположить, что приведенный выше Rust код является частью пакета {extendrtest}, то становятся досутпны следующие функции:

extendrtest::add_i32(4L, 11L)# [1] 15extendrtest::add_vec(1:10, 10:1)#  [1] 11 11 11 11 11 11 11 11 11 11

Да, насктолько просто.

Шаг третий: user-friendliness

В своей работе мы вдохновлялись такими проектами как {cpp11} - header-only пакет для интеграции C++11 кода. Так появился на свет {rextendr}, R - пакет без Rust-зависимости, который решает три основные задачи:

  • Создание шаблона пакета, использующего extendr, наподобие {usethis};

  • Компиляция и исполнение Rust - кода на лету, прямо в R-сессии. Именно это демонстрирует Анимация Для Привлечения Внимания;

  • Предоставление специальных knitr-модулей (engines), а именно {extendr} и {extendrsrc}, которые позволяют включать фрагменты Rust-кода (и результаты его выполнения) в ваш Rmarkdown прямо рядом с R-кодом, обеспечивая их взаимодействие.

Сейчас {rextendr} отправился на проверку в CRAN, и мы ждем результатов. Я думаю, самое время продемонстроровать несколько примеров именно с использованием {rextendr}. Сразу оговорюсь, для упрощения и воспроизводимости, я буду использовать{reprex}.

Самый простой пример это, конечно же,

rextendr::rust_function("fn hello_r() -> &'static str { \"Hello R!\" }")#> i build directory: 'C:\Users\...\AppData\Local\Temp\Rtmp259cVM\file10186cb44264'#> v Writing 'C:/Users/.../AppData/Local/Temp/Rtmp259cVM/file10186cb44264/target/extendr_wrappers.R'.hello_r()#> [1] "Hello R!"

Пример со сложением я уже показывал, но что будет, если явно передать NA?

rextendr::rust_function("fn add_i32(x : i32, y : i32) -> i32 { x + y }")#> i build directory: 'C:\Users\...\AppData\Local\Temp\Rtmp2P2cnQ\file2f7c65e8269a'#> v Writing 'C:/Users/.../AppData/Local/Temp/Rtmp2P2cnQ/file2f7c65e8269a/target/extendr_wrappers.R'.add_i32(42L, NA)#> Error in add_i32(42L, NA): unable to convert R object to primitive

Сообщения об ошибках еще не очень информативны, мы работаем над этим, но тем не менее мы получаем базовую валидацию просто за счет системы типов -- NA не совместим с i32. Однако, можно написать вот так

rextendr::rust_function("fn add_i32_opt(x : Option<i32>, y : Option<i32>) -> Option<i32> {    match (x, y) {        (Some(a), Some(b)) => Some(a + b),        _ => None    }}")#> i build directory: 'C:\Users\...\AppData\Local\Temp\Rtmpyg3uPw\file6587a897d2a'#> v Writing 'C:/Users/.../AppData/Local/Temp/Rtmpyg3uPw/file6587a897d2a/target/extendr_wrappers.R'.add_i32_opt(NA, 42L)#> [1] NAadd_i32_opt(42L, 100L)#> [1] 142

Хотите еще больше магии? Макрос R! выполняет внутри R-код, возвращая результат если операция была успешной. Как насчет

x <- 42L y <- 100Lrextendr::rust_eval("R!(x)? * 2 + R!(y)? * 3")#> i build directory: 'C:\Users\...\AppData\Local\Temp\RtmpKeC23J\file32ec53677fc9'#> v Writing 'C:/Users/.../AppData/Local/Temp/RtmpKeC23J/file32ec53677fc9/target/extendr_wrappers.R'.#> [1] 384

Можно попробовать смешать переменные из R и Rust.

library(tibble)x <- 10:1 # Эта переменная на стороне Rrextendr::rust_eval("call!(\"tibble\", x = R!(x), y = 1..=10)")#> i build directory: 'C:\Users\...\AppData\Local\Temp\RtmpcDWhlk\file45802f52dc5'#> v Writing 'C:/Users/.../AppData/Local/Temp/RtmpcDWhlk/file45802f52dc5/target/extendr_wrappers.R'.#> # A tibble: 10 x 2#>        x     y#>    <int> <int>#>  1    10     1#>  2     9     2#>  3     8     3#>  4     7     4#>  5     6     5#>  6     5     6#>  7     4     7#>  8     3     8#>  9     2     9#> 10     1    10

Эти макросы полезны, но до сих пор нестабильны. Они лишь демонстрируют потенциальные возможности для взаимодействия R и Rust.

Для безопасной печати в Rout существует отдельный макрос : rprintln!.

x <- 42Lrextendr::rust_eval("rprintln!(\"Hello from Rust! x = {}\", R!(x)?.as_integer().unwrap());")#> i build directory: 'C:\Users\...\AppData\Local\Temp\RtmpWQh3w0\file48e024f161ce'#> v Writing 'C:/Users/.../AppData/Local/Temp/RtmpWQh3w0/file48e024f161ce/target/extendr_wrappers.R'.#> Hello from Rust! x = 42

Пишем свой extendr-пакет

В этом разделе я просто приведу пример генерации пакета с использование {rextendr} и других стандартных инструментов:

pkg <- file.path(tempfile(), "myextendr")dir.create(pkg, recursive = TRUE)usethis::create_package(pkg)usethis::proj_activate(pkg)rextendr::use_extendr()rextendr::document()rextendr::document()hello_world()
Как это выглядит
pkg <- file.path(tempfile(), "myextendr")dir.create(pkg, recursive = TRUE)usethis::create_package(pkg)#> v Setting active project to 'C:/Users/.../AppData/Local/Temp/RtmpAVW4HZ/file122c180d1953/myextendr'#> v Creating 'R/'#> v Writing 'DESCRIPTION'#> Package: myextendr#> Title: What the Package Does (One Line, Title Case)#> Version: 0.0.0.9000#> Authors@R (parsed):#>     * First Last <first.last@example.com> [aut, cre] (YOUR-ORCID-ID)#> Description: What the package does (one paragraph).#> License: `use_mit_license()`, `use_gpl3_license()` or friends to pick a#>     license#> Encoding: UTF-8#> LazyData: true#> Roxygen: list(markdown = TRUE)#> RoxygenNote: 7.1.1#> v Writing 'NAMESPACE'#> v Setting active project to '<no active project>'usethis::proj_activate(pkg)#> v Setting active project to 'C:/Users/.../AppData/Local/Temp/RtmpAVW4HZ/file122c180d1953/myextendr'#> v Changing working directory to 'C:/Users/.../AppData/Local/Temp/RtmpAVW4HZ/file122c180d1953/myextendr/'rextendr::use_extendr()#> v Creating 'src/rust/src'.#> v Writing 'src/entrypoint.c'#> v Writing 'src/Makevars'#> v Writing 'src/Makevars.win'#> v Writing 'src/.gitignore'#> v Writing 'src/rust/Cargo.toml'.#> v Writing 'src/rust/src/lib.rs'#> v Writing 'R/extendr-wrappers.R'#> v Finished configuring extendr for package myextendr.#> * Please update the system requirement in 'DESCRIPTION' file.#> * Please run `rextendr::document()` for changes to take effect.rextendr::document()#> i Generating extendr wrapper functions for package: myextendr.#> ! No library found at 'src/myextendr.dll', recompilation is required.#> Re-compiling myextendr#>   -  installing *source* package 'myextendr' ...#>      ** using staged installation#>      ** libs#>      rm -Rf myextendr.dll ./rust/target/x86_64-pc-windows-gnu/release/libmyextendr.a entrypoint.o#>      "C:/rtools40/mingw64/bin/"gcc  -I"C:/PROGRA~1/R/R-41~1.0/include" -DNDEBUG          -O2 -Wall  -std=gnu99 -mfpmath=sse -msse2 -mstackrealign  -UNDEBUG -Wall -pedantic -g -O0 -c entrypoint.c -o entrypoint.o#>      cargo build --target=x86_64-pc-windows-gnu --lib --release --manifest-path=./rust/Cargo.toml#>              Updating crates.io index#>             Compiling winapi-build v0.1.1#>       Compiling winapi v0.3.9#>       Compiling winapi v0.2.8#>       Compiling proc-macro2 v1.0.27#>       Compiling unicode-xid v0.2.2#>       Compiling syn v1.0.73#>       Compiling extendr-engine v0.2.0#>       Compiling lazy_static v1.4.0#>             Compiling kernel32-sys v0.2.2#>             Compiling quote v1.0.9#>             Compiling extendr-macros v0.2.0#>             Compiling libR-sys v0.2.1#>             Compiling extendr-api v0.2.0#>             Compiling myextendr v0.1.0 (C:\Users\...\AppData\Local\Temp\RtmpAVW4HZ\file122c180d1953\myextendr\src\rust)#>              Finished release [optimized] target(s) in 33.09s#>      C:/rtools40/mingw64/bin/gcc -shared -s -static-libgcc -o myextendr.dll tmp.def entrypoint.o -L./rust/target/x86_64-pc-windows-gnu/release -lmyextendr -lws2_32 -ladvapi32 -luserenv -LC:/PROGRA~1/R/R-41~1.0/bin/x64 -lR#>      installing to C:/Users/.../AppData/Local/Temp/RtmpAVW4HZ/devtools_install_122c37bd1965/00LOCK-myextendr/00new/myextendr/libs/x64#>   -  DONE (myextendr)#> v Writing 'R/extendr-wrappers.R'.#> i Updating myextendr documentation#> i Loading myextendr#> Writing NAMESPACE#> Writing NAMESPACE#> Writing hello_world.Rdrextendr::document()#> i Generating extendr wrapper functions for package: myextendr.#> i 'R/extendr-wrappers.R' is up-to-date. Skip generating wrapper functions.#> i Updating myextendr documentation#> i Loading myextendr#> Writing NAMESPACE#> Writing NAMESPACEhello_world()#> [1] "Hello world!"

hello_world() написана на Rust и автоматически экспортируется в R. Обратите внимание, что hello_world.Rd был создан при вызове rextendr::document() (аналог devtools::document()). Дело в том, что rextendr-парсер воспринимает /// комментарии как R комментарии. Rust функция выглядит вот так

/// Return string `"Hello world!"` to R./// @export#[extendr]fn hello_world() -> &'static str {    "Hello world!"}

Что автоматически генерирует R обертку

#' Return string `"Hello world!"` to R.#' @exporthello_world <- function() .Call(wrap__hello_world)

и, как результат, обновляет документацию и NAMESPACE с помощью {roxygen2}.

Если этого мало

Здесь я хотел бы коротко описать последнюю важную фичу extendr. Крейт позволяет экспортировать не просто функции, а целые типы. Легким движением руки можно пробросить кастомный тип из Rust в R , а инстансы этого типа -- передавать в обе стороны как ссылки. Это позволяет заполучить ООП в R в традиционном (object-first) стиле, модицифируя in-place объекты, созданные и доступные из Rust:

Мутабельный объект
rextendr::rust_source(code = "struct Counter {    n: i32,}#[extendr]impl Counter {    fn new() -> Self {        Self { n: 0 }    }        fn increment(&mut self) {        self.n += 1;    }        fn get_n(&self) -> i32 {        self.n    }}")#> i build directory: 'C:\Users\...\AppData\Local\Temp\RtmpWOu1pt\file5318783e2176'#> v Writing 'C:/Users/.../AppData/Local/Temp/RtmpWOu1pt/file5318783e2176/target/extendr_wrappers.R'.cntr <- Counter$new()cntr$get_n()#> [1] 0cntr$increment()cntr$increment()cntr$get_n()#> [1] 2

Вместо заключения

Статья получилась гораздо длиннее и сумбурней, чем я ожидал. Тем не менее, я не успел описать все возможности extendr. Этот проект амбициозный и еще далек от завершения, но я считаю, что давно пришло время добавить поддержку Rust в R, а главное сделать взаимодействие этих языков удобным. Мы осторожно надеемся, что в конечном итоге сможем добавить официальную поддержку Rust, на равне с C / C++. К сожалению, сейчас ее отсутствие накладывает на нас некоторые ограничения.

Отдельным вызовом было заставить эту систему работать на Windows. Мы столкнулись со множеством проблем, но на данный момент нам удалось справиться практически со всеми трудностями. Для запуска на Windows extendr требует стандартный Rust - тулчейн, stable-x86_64-pc-windows-msvc, с дополнительными целями (targets) x86_64-pc-windows-gnu и i686-pc-windows-gnu, а также Rtools40v2 (последняя версия на момент написания, отличается от Rtools40).

Скудную документацию можно найти здесь и в репозиториях проекта extendr.

Спасибо что дочитали до конца!

Подробнее..
Категории: Rust , R , Package , Interop , Crate

JuliaR преимущества интеграции

24.03.2021 08:16:47 | Автор: admin

R стабильный, удобный, имеет прекрасную экосистему. Один из признанных лидеров в области анализа и визуализации данных и, наверное, лучший инструмент для статистических исследований. Julia тоже удобна, весьма стройна и при этом ещё и быстра. Она ещё очень молода, не имеет такого количества пакетов и пользователей, но скорость работы впечатляет. Особенно по сравнению с R. Оба языка активно используются для анализа данных и машинного обучения. Что же выбрать?

Сопоставление производительности набора программ, реализованных на разных языках программирования, относительно производительности их реализаций на языке Chttps://julialang.org/benchmarks/Сопоставление производительности набора программ, реализованных на разных языках программирования, относительно производительности их реализаций на языке Chttps://julialang.org/benchmarks/

А что, если вместо неявно навязанного состязания выбрать оба языка и использовать преимущества и того, и другого? Ведь такая возможность есть. К тому же эта интеграция настолько проста, что особых усилий прикладывать и не нужно: несколько минут и всё готово.

Зачем вообще что-то интегрировать? Почему нельзя просто использовать R (Julia)?

Кратко рассмотрим преимущества обоих языков.

R удобен, стабилен, имеет богатую коллекцию пакетов на все случаи жизни. Он давно стабилизировался, поэтому его можно изучать просто по рецептам из сети и книг: они все работают. Есть много высококлассной литературы, статей, лекций, учебных курсов, что очень упрощает процесс освоения языка и дальнейшее совершенствование мастерства. Если не рассматривать случай каких-то очень больших массивов данных или сложных вычислительных экспериментов, то на производительность жаловаться не приходится, она для большинства задач вполне приемлемая. Поскольку язык интерпретируемый, все команды пользователя обрабатываются на лету, как правило без ощутимых задержек, что делает его очень удобным для разведочного анализа данных. Сочетание удобства языка и скорости выполнения команд дают ощущение взаимодействия с каким-то интеллектуальным ассистентом, который моментально выполняет все твои мысли. Это приятное ощущение.

Julia тоже удобна, тоже имеет приятный синтаксис, немного более строга к пользователю, но имеет свою особенную стройность. Из уникальных особенностей стоит отметить концепцию множественной диспетчеризации, возможность лёгкого доступа к генерируемому низкоуровневому коду, вплоть до ассемблерного, удобную систему макросов. Если бы этим качества и ограничивались, то она была бы просто аналогом R, но Julia при этом очень производительна, даже по меркам компилируемых языков. Вместе с С, C++ и Fortran Julia входит в престижный клуб языков с петафлопсной производительностью. Код на Julia можно запускать как на обычных многоядерных CPU, так и на GPU или вычислительных кластерах. Язык активно используется при проведении научных симуляций, обработке больших массивов данных, задачах машинного обучения. По сравнению с R аналогичные программы на Julia почти всегда в десятки-сотни раз быстрее. При этом благодаря JIT-компиляции команды выполняются почти на лету, как правило без серьёзных задержек. Не так молниеносно, как в R, задержки иногда всё же возникают, Julia как бы берёт некоторую паузу на подумать, особенно когда по ходу дела вдруг надо скомпилировать какой-нибудь пакет. Но подумав и перейдя к вычислениям, Julia работает с очень высокой производительностью. А существенные задержки, во-первых, случаются не так часто и поэтому не слишком раздражают, и к тому же при желании их можно максимально сократить, а во-вторых, ведётся работа над тем, чтобы повысить отзывчивость исполнения команд. Из-за сочетания производительности и удобства использования популярность Julia стремительно набирает обороты. В рейтингах TIOBE и PYPL язык уверенно движется к двадцатке лидеров и имеет вполне реальные шансы войти в неё уже в этом году. С августа 2018 года, когда вышла версия 1.0 и язык в значительной степени стабилизировался, прошло уже достаточно времени, чтобы он перестал ассоциироваться с чем-то сырым и постоянно меняющимся, появилась какая-то литература по актуальной версии. С другой стороны, многие рецепты из сети просто не работают, так как были опубликованы во времена старых версий и язык с тех пор уже претерпел многие изменения. Пока что по современным версиям языка очень мало литературы (на русском по версиям 1.0 и выше печатных изданий пока что вообще нет), нет такой прозрачной системы документации, как у R, далеко не для всех проблем можно найти в сети готовые красивые решения. С Julia постоянно чувствуешь себя первооткрывателем. Но это тоже приятное ощущение.

Сопоставление популярности языков R и Julia по данным рейтинга PYPLhttps://pypl.github.io/PYPL.htmlСопоставление популярности языков R и Julia по данным рейтинга PYPLhttps://pypl.github.io/PYPL.html

Что может дать использование Julia пользователям R

Julia явно быстрее, поэтому на ней разумно производить все ресурсоёмкие вычисления. Есть рецепты, как делать в R вставки на Fortran или C++, но по сравнению с ними Julia имеет гораздо более близкий к R синтаксис, вплоть до прямого заимствования отдельных команд из R. При этом по производительности она находится где-то на уровне близком к C и как правило обгоняет Fortran. В отличие от Fortran, С и C++ код в Julia компилируется на лету. Это позволяет сохранить высокую интерактивность работы, привычную после опыта в R. Вплоть до того, что в Atom и VS Code наиболее популярных средах разработки для Julia, можно точно так же, как в RStudio (наиболее популярной среде разработки для R) отправлять на исполнение произвольные куски кода по Ctrl+Enter. То есть общий подход к работе и ощущение от работы остаются примерно такими же, как и в R, но становится доступна очень высокая производительность.

У Julia есть потенциал, чтобы стать универсальным языком учёных будущего, каким раньше был Fortran. Похоже, ставка авторов Julia на сочетание скорости, удобства и открытости вполне оправдалась. Современный R несмотря на свою приятность сильно ограничен максимально возможной производительностью. Ради оптимизации под производительность приходится отказываться от циклов и писать все ресурсоёмкие процедуры в векторизованном стиле, из-за чего код становится тяжёлым для чтения и отладки. Но даже в максимально оптимизированном виде производительность далеко отстаёт от компилируемых языков. Ведётся работа над повышением производительности, но результат как правило нужен уже сейчас, поэтому приходится искать инструменты, которые прямо сейчас позволяют достичь его. Julia позволяет полноценно использовать циклы, предлагает простые средства для организации параллельных вычислений. Интересной особенностью экосистемы Julia являются интерактивные блокноты Pluto, позволяющие создавать красиво свёрстанные интерактивные научные презентации c формулами в LaTeX и выполняющимися на лету компьютерными симуляциями, меняющими своё поведение в зависимости от изменяемых в ходе показа параметров. Наверное, как-то так должны выглядеть научные публикации будущего.

Допустим, всё это звучит убедительно. Но выбрасывать все свои наработки на R ради освоения какого-то нового языка как-то не хочется. Хорошая новость в том, что если вместо перехода на Julia интегрироваться с ней, то можно ничего не выбрасывать, а всё так же, как и раньше, использовать все свои программы, шаблоны графики, подключаемые библиотеки. Всё это будет работать, как и раньше, и не потребует никаких модификаций.

Что может дать использование R пользователям Julia

Даже если Julia это универсальный язык учёных будущего, жить и работать учёным всё же приходится в настоящем. И в настоящем стоит отметить очень юный возраст Julia. Несмотря на то, что к настоящему времени экосистема языка и основных пакетов более-менее стабилизировалась, такого багажа наработок, который есть для R, удастся достичь ещё нескоро. R полезен хотя бы тем, что он используется как основной язык для демонстрации излагаемых концепций в учебных курсах и специализированной литературе, посвящённой решению конкретных прикладных задач. Есть прекрасные издания по анализу и визуализации данных, статистическому обучению, картографии и другим практическим областям, иллюстрированные примерами на R, в том числе - с использованием уникальных для R инструментов. В некоторых случаях подобные издания могут давать уникальную возможность получения знаний непосредственно из первых рук, от авторов предлагаемых методов. Можно подождать несколько лет, пока что-то подобное появится и для Julia, но при совместном использовании можно и не ждать, а использовать всё прямо сейчас.

Благодаря своим пакетам R давно уже превратился в своеобразный швейцарский нож, современная функциональность которого просто поражает. Вряд ли Julia в ближайшие годы сможет догнать R по количеству и качеству доступных пакетов, особенно в области статистики, анализа пространственных данных, картографии. Но зачем терять время, если все пакеты из R можно использовать уже сейчас? Да, их можно подключить и использовать практически как свои родные. В частности, это даёт возможность сопоставления результатов работы совсем недавно стабилизировавшихся пакетов Julia с результатами работы многократно проверенных пакетов R.

Отдельная тема - геоинформатика. Здесь R уже давно является одним из отраслевых стандартов. В некотором смысле, современный R с соответствующими пакетами можно рассматривать как открытую геоинформационную систему с текстовым интерфейсом, причём достаточно популярную и мощную. Вполне реальна ситуация, когда заходишь на какой-нибудь портал скачать необходимые для работы открытые пространственные данные, а там помимо готовой карты в pdf и самих данных лежит ещё и скрипт на R, который по ним строит правильно оформленную карту. Хотя в среде Julia тоже наблюдается определённое движение в направлении геоинформатики, до уровня R тут пока что ещё далеко. Но, опять же, при желании можно не ждать, а взять всё сразу. Взять всё сразу - это очень в стиле философии Julia.

Что для этого нужно сделать

Очевидно, есть два подхода к решению проблемы: вызывать код R из Julia и вызывать код Julia из R. Для вызова R из Julia есть специальный пакет Rcall. Для вызова Julia из R существует сразу несколько альтернативных решений: пакеты XRJulia, JuliaCall, RJulia и JuliaConnectoR. На данный момент наиболее прогрессивным из них выглядит JuliaConnectoR. Предлагаемый им подход немного тяжеловесен, и ориентирован скорее на использование отдельных функций из Julia, чем на полностью смешанное использование двух языков. Довольно любопытны доводы авторов в польу использования именно R, как связующей среды, опубликованные в их статье на arxiv.org.

Изначально я как раз планировал вызвать Julia из R. Потратил много времени, но так и не смог добиться работоспособности этой конструкции ни с XRJulia, ни с JuliaCall. Какой-либо информации про JuliaConnectoR тогда ещё не было, поэтому не было уверенности, что сама идея совместного использования этих двух языков - здравая. Всё шло к тому, что тяжёлые расчётные части придётся переписать с R на Fortran, с которым соприкасался когда-то в институте, но с тех пор никогда больше не использовал. Просто ради интереса решил попробовать вызвать R из Julia, и каково же было моё удивление, когда всё заработало практически из коробки и оказалось действительно удобным способом совместного использования двух языков. Возможно, это мой субъективный опыт, но именно на этом способе мне хочется сейчас остановиться. Про вызов Julia из R, возможно, расскажу как-нибудь в другой раз.

Итак, что нам понадобится:

1) Установленная Julia

2) Установленный R

3) В Julia необходимо установить пакет Rcall: Pkg.add("RCall")

4) Необходимо прописать путь до R. В Windows это удобнее всего сделать, задав переменную среды. В моём случае: R_HOME = C:\Program Files\R\R-4.0.3

Ну вот, собственно, и всё. Наш волшебный гибрид готов! Теперь в любом месте внутри кода Julia можно вызывать код на R и это будет работать. Можно даже не закрывать RStudio с запущенным сеансом в R. Вызов из Julia будет обрабатываться независимо от него.

Подключаем установленную библиотеку Rcall:

using RCall

Вызываем одиночную команду на R. Если R в результате выполнения команды должен построить график, то график откроется в отдельном окне:

R"plot(rnorm(10))"

Можно последовательно запускать отдельные команды, так, как будто мы работаем в консоли R, и они будут выполняться внутри единого сеанса:

R"var <- 5*8"

R"var"

R"var / 100"

Фактически это и есть сеанс в R, только обёрнутый в команды Julia. На каком языке в данном случае ведётся работа это скорее уже философский вопрос.

Естественно, дальше нам захочется как-то передавать переменные из одного языка в другой. Для этого удобно использовать команды @rput и @rget. Забрасываем какие-то переменные в R, что-то там считаем, забираем результаты обратно:

a = 1

@rput a

R"b <- a*2 + 1"

@rget b

Если необходимо выполнить последовательность команд, например целый лист кода, то удобнее использовать вставку вида:

R"""

"""

Например:

a = 1

@rput a

R"""

b <- a*2 + 1

c <- b*3

"""

@rget c

Внутри вызванного кода можно вызывать любые библиотеки R, и это будет работать так же, как и в обычном сеансе R.

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

Есть ли здесь какие-то подводные камни? Я нашёл пока только два:

В текущей версии не поддерживаются кириллические комментарии в R. Придётся их поудалять или заменить на латинские.

В интерактивных блокнотах Pluto, силе и гордости экосистемы Julia, поддержка R работает странно. При первом запуске всё работает, но при изменении входных данных или содержимого R скрипта вместо обновления порождаемых им объектов происходит разрыв коннекта с R. Я пока что не нашёл способа, как обойти это ограничение.

Ещё небольшой комментарий: для того, чтобы комфортно работать с подобными двуязычными проектами, необходимо, чтобы среда разработки поддерживала оба языка и их совместное использование. Как Atom (прошлый фаворит Джулии, с которым она рассталась летом), так и VS Code (нынешний фаворит) при установке синтаксических дополнений для обоих языков такой режим поддерживают. Но очень может быть, что в каких-то средах такой поддержки не будет. Это может стать проблемой, потому что осуществлять навигацию по простыне кода, оформленного как один огромный комментарий, не очень удобно.

На этом всё! Надеюсь, это небольшая заметка была полезна

Подробнее..

Проверка гипотезы равенства средних при неравной дисперсии в R

11.05.2021 00:16:11 | Автор: admin

(при условии нормальности распределения)

Задача определения равенства средних при условии равных дисперсий - классическая задача математической статистики, которую решают в техникумах и ВУЗах. Однако МС как наука очень похожа на болото - при попытке спрыгнуть в сторону с кочки классически решаемой задачи можно увязнуть или вовсе утонуть

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

Проведенное предварительное исследование (текст исследования доступен на GitHub) показало, что в зависимости от конкретной комбинации значений средних, дисперсии, и особенностей постановки задачи лучшим может быть чуть ли не любой из тестов, рассмотренных в статье "Cavus, M., Yazici, B. Testing the equality of normal distributed and independent groups' means under unequal variances by doex package / The R Journal. 2020. 2 (12). P. 134-155".

Для решения этой задачи была разработана процедура, позволяющая для каждого конкретного случая определить лучший статистический тест. Она будет продемонстрирована на примере базы данных GrowthDJ, содержащих данные об экономическом росте. Проверим предположение о равенстве средних значений экономического роста (переменная gdpgrowth) в зависимости от наличия в странах качественных данных (переменная inter)

Первые этапы исследования - проверка нормальности распределений и нахождение описательных статистик:

library("tibble")

library("AER")

library("WRS2")

library("doex")

data("GrowthDJ")

XX<-na.omit(GrowthDJ)

library("psych")

describeBy(XX$gdpgrowth, XX$inter)

shapiro.test(XX[XX$inter=='yes',6])

shapiro.test(XX[XX$inter=='no',6])

Получаем, что наши данные распределены нормальны - значит, тесты можно применять

Методика проверки

  1. Задается два средних значения и два значения дисперсии (исходя из имеющихся данных по группам)

  2. При заданных средних значениях и заданных дисперсиях генерируется три нормально распределенных выборки (каждая из 70 наблюдений). Первая со средним значением 1 и дисперсии 1, вторая со средним значением 1 и дисперсией 2, третья со средним значением 2 и дисперсией 2.

  3. Далее с использованием выбранного теста по объединенным данным первой и третьей выборки проверяется гипотеза о равенстве средних на уровне значимости в 0.01. Если полученное p-значение критерия больше 0.01, то гипотеза отклоняется верно, если меньше 0.01 то гипотеза ошибочно принята. По объединенным данным первой и второй выборки также проверяется данная гипотеза. Если полученное p-значение критерия меньше 0.01, то гипотеза принимается верно, если больше 0.01 то гипотеза ошибочно отклонена. После этого данная симуляция проводится 100 раз по каждому тесту, и подводится количество разных исходов.

На основании полученных результатов выявляются лучшие тесты по следующим метрикам качества (рассчитаны по аналогии с соответствующими показателями, используемыми при анализе качества классификации):

  • accuracy (процент правильно сделанных выводов);

  • selectivity (доля правильных выводов для выборок, в которых средние не равны);

  • precision (доля правильно сделанных выводов о равенстве средних);

  • recall (доля правильных выводов для выборок, в которых средние равны);

  • FOR (доля ошибочно сделанных выводов о неравенстве средних);

  • F-мера (гармоническое среднее precision и recall, общая характеристика точности качества).

Результаты расчетов (код представлен в файле Итого.R на гитхабе) позволили получить следующую таблицу

Выведем лучшие тесты

Таким образом, получаем:

  • если нам нужен самый точный тест вообще, то необходимо использовать AF или FA-тест (они лучшие и по точности, и по значению F-score

  • если необходимо максимально избегать ложно-отрицательного вывода (т.е. ложного вывода о наличии различий в средних), необходимо применять RGF-тест

  • если необходимо максимально избегать ложно-положительного вывода (т.е. ложного вывода о равенстве средних), можно выбрать любой из 8 тестов (AF,BA,CF,FA,JF,MBF,SS,WA)

  • если нам нужна максимальная точность вывода о равенстве средних, то также можно выбрать любой из 8 тестов

  • если нам нужна максимальная точность вывода о неравенстве средних, то также необходимо применять RGF-тест

Итого - давайте применим AF-тест (Approximate F-test)

Гипотеза о равенстве средних принята на уровне значимости 0.0003 или в терминах поставленной задачи - среднее значение темпов экономического роста не отличается в странах с наличием или отсутствием более качественных данных

Подробнее..

Продуктовая аналитика на практике при помощи R

26.02.2021 14:22:55 | Автор: admin

Проблематика

Современная мета управления продуктом подразумевает управление на основании данных.

Все хотят аналитического подхода и способности принимать решения на куче данных о продукте которые есть. Но в реальности есть недостаток информации о том, как именно это делать. Какие инструменты использовать, как принимать решения и как исследовать данные. Я бы хотел поделится именно практическим аспектом этого вопроса в формате одного кейса.

Вводная часть

Начало 2020 года, вы - обычный продукт-менеджер, которому предложили развивать кредитный продукт в другой стране. Оффер принят, документы оформлены, пора за работу.

Первое, что приходит в голову посмотреть, что там с экономикой продукта. И как вообще ведет себя продукт.

Еще через пару дней совещание, на котором вас просят ответить на несколько вопросов:

  1. Сейчас мы платим за привлечение клиента 695494. Какая стоимость привлечения для нас является приемлемой? Имеет ли смысл повысить стоимость привлечения на клиента, чтобы получить больший объем?

  2. Насколько здоровой выглядит экономика портфеля и какая динамика здесь и сейчас?

  3. Мы недавно изменили подход к размеру выдачи и стали в первые кредиты выдавать меньшие чеки. Как это сказалось на продукте?

В общем-то понятные вопросы, на которые стоит уметь отвечать любому владельцу продукта.

Но есть проблема. Метрики, конечно, все посчитаны: LTV, CAC и прочее. Есть финансовая отчетность, где видны все расходы и доходы, и видно, что продукт в небольшом операционном плюсе.

Но на вышеозначенные вопросы ответить сложно. Поиск ответов может занять кучу встреч и разговоров (которые еще и в меру субъективны), задач на аналитику(итерации чего занимают время) и попыток прокрутить дашборд (интерпретация которых может быть неверной).

Ответить на вопросы можно быстро и однозначно, четко посмотрев, что там в сырых данных. Большинство процессов в продукте оставляют следы в базах данных.

А ядро почти любого финансового продукта - транзакции, в них и предлагается смотреть ниже.

Анализируй это

Пример взят из реальной практики. То есть вот прям все взаправду, данные подшумлены, но это реальный портфель. И анализируются данные примерно таким образом.

Для того, чтобы анализировать, понадобятся следующие инструменты: R ,Rstudio,dbeaver(или аналог). Далее рассматривается реальный пример, как могут выглядеть эти данные и что с ними можно сделать.

Итак, транзакции. Напишем в базу нечто вроде select * from transactions t и посмотрим результат.

Rows: 2,226,532Columns: 10$ borrower_id          2, 2, 2, 6, 6, 12, 12, 12, 12, 16, 20, 20, 20, 20, 22, 23, 23, 33, 33, 39, 39, 36, 36$ con_id               1, 1, 1, 2, 2, 4, 4, 4, 4, 5, 7, 7, 7, 7, 8, 9, 9, 10, 10, 11, 11, 12, 12, 12, 12, 13$ disbursement_date    2017-11-23, 2017-11-23, 2017-11-23, 2017-11-24, 2017-11-24, 2017-11-27, 2017-11-27, $ prolongations_count  1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1$ loan_type            "pdl", "pdl", "pdl", "pdl", "pdl", "pdl", "pdl", "pdl", "pdl", "pdl", "pdl", "pdl", "$ date                 2017-12-15, 2017-11-23, 2017-12-06, 2017-11-24, 2017-12-25, 2017-12-29, 2017-11-27, $ type                 "Payments::Transaction::ContractAddTransaction", "Payments::Transaction::DisburseTran$ amount               250000, 1000000, 1200000, 2500000, 3500000, 1040000, 1500000, 10000, 1470000, 1500000$ id                   325, 2, 127, 5, 587, 557500, 557499, 557504, 557507, 17, 182865, 182874, 182869, 1828$ deleted_at           NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, N

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

У нас есть идентификаторы заемщика, контракта, и транзакции. Дата выдачи кредита (disbursment_date). Отметка о пролонгации кредита (prolongations_count) - перенос даты выплаты за отдельный платеж. Размер и дата транзакции. Отметка о "мягком" удалении.

Главное - тип транзакции(ContractAdd- возврат денег заемщиком, DisburseTransaction - выплата денег заемщику).

Теперь поговорим об экономике продукта. Принципиально кредитный бизнес работает просто(как и большинство бизнесов): заплатили, чтобы привести пользователя -> выдали кредит -> собрали деньги -> выдали еще кредит.

Выручку по продукту формируют два финансовых потока: от нас к пользователю и от пользователя к нам.

И в нашей табличке есть все данные, чтобы это посчитать.

Также узнаем курс местной валюты к доллару(для примера возьмем, что курс без динамики и составляет 0.00003 доллара за местную валюту).

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

  • для любителей R, данных в табличке прилично, больше 2 млн. Для таких датасетов предпочитаю data table. Но можно и на tidyverse(по запросу покажу как)

Теперь посмотрим на структуру данных и займемся обработкой.

Чуть обработаем их и приведем данные в нужный нам формат. Назовем транзакции удобно и сделаем отрицательными транзакции выдачи кредитов (поля z_type и am):

lk %>% data.table()->lk1 #Создаем отдельный объект вида data tablelk1[,':='(z_type=z_type<-fifelse(#  создаем отдельную переменную - которая отвечает за тип транзакций type=='Payments::Transaction::ContractAddTransaction','add','disb'),am=amount*fifelse(z_type=='add',1,-1))][1:20,c(-1,-11,-8,-6)] #убираем ненужные колонкиlk1[disbursement_date<'2020-01-01' & date<='2020-03-01',c(-1,-11,-8,-6)]->lk1glimpse(lk1) #посмотрим на получившийся фаил
$ borrower_id          2, 2, 2, 6, 6, 12, 12, 12, 12, 16, 20, 20, 20, 20, 22, 23, 23, 33, 33, 39, 39, 36, 36$ con_id               1, 1, 1, 2, 2, 4, 4, 4, 4, 5, 7, 7, 7, 7, 8, 9, 9, 10, 10, 11, 11, 12, 12, 12, 12, 13$ disbursement_date    2017-11-23, 2017-11-23, 2017-11-23, 2017-11-24, 2017-11-24, 2017-11-27, 2017-11-27, $ prolongations_count  1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1$ date                 2017-12-15, 2017-11-23, 2017-12-06, 2017-11-24, 2017-12-25, 2017-12-29, 2017-11-27, $ amount               250000, 1000000, 1200000, 2500000, 3500000, 1040000, 1500000, 10000, 1470000, 1500000$ id                   325, 2, 127, 5, 587, 557500, 557499, 557504, 557507, 17, 182865, 182874, 182869, 1828$ z_type               "add", "disb", "add", "disb", "add", "add", "disb", "add", "add", "disb", "disb", "ad$ am                   250000, -1000000, 1200000, -2500000, 3500000, 1040000, -1500000, 10000, 1470000, -150

Давайте посмотрим на главное и посчитаем, прибыльный ли портфель.

Для этого посмотрим транзакции за все время. И так как цифры крупные, посмотрим сразу в миллионах:

lk1[,.(total_in_mln=sum(am*for_ex)/1e6),.(z_type)]

total_in_mln

add

58.33176

disb

-45.49114

Видим ясный и понятный результат: транзакции от заемщиков (add) значительно превышают транзакции от выдачи кредита (disb). А ведь часть кредитов наверняка только выдана и по ним даже не подошла дата выплаты.

И здесь же разберемся с темпом, с которым выдавали кредиты:

lk1[!is.na(disbursement_date)&z_type=='disb',.(sum=sum(am*for_ex*-1,na.rm = T)/1e6),.(date=floor_date(disbursement_date,'month',))][,ggplot(.SD,aes(date,sum,label=round(sum,2)))+geom_col(fill=polar_night[2])+ff+tt+  geom_text(aes(y=sum+0.1),col=aurora[1])+  labs(x='месяц выдачи',y='выдача в миллионах долларов',  title='Выдача кредитов в месяц')]

Аналитически видно, что масштабирование продукта в последние несколько месяцев остановилось. Выдаем столько же сколько и выдавали.

Вернемся к Юнит экономике

Глобально мы занимаемся масштабированием пользователей, ведь кредит без человека выдать сложно.

А юнит экономика в таком контексте- это сколько мы зарабатываем или теряем на одном пользователе, заплатив за его привлечение.

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

Удобнее работать с относительным временем, так сможем сравнивать пользователей, пришедших в разное время:

lk1[][,':='( min_date=min(disbursement_date)),.(borrower_id)][,#найдем дату начала работы каждого заемщикаc("gen",'dif'):=.(floor_date(min_date,'1 month'),as.numeric(date-min_date))][, #месяц в котором появился заемщик и растояние в днях между датой его рождения и датой транзакцииn:=uniqueN(borrower_id),][, #количество уникальных клиентов  .(sum=sum(am*for_ex),n=unique(n)),  .(dif)][order(dif)][,":="(bal=bal<-sum/n,cum=cumsum(bal))][,    ggplot(.SD,aes(dif,cum))+ #строим график   geom_line()+   tt+ff+        labs(x='дни жизни заемщика',y='доход в USD',col='поколение',        title='LTV клиента по поколениям и по годам')+   scale_y_continuous(breaks = seq(-200,200,20),   labels =paste0('$',seq(-200,200,20),'k' ))+   scale_x_continuous(breaks = seq(0,1000,20))  ]

Получившаяся кривая показывает его LTV c динамикой по времени. И это уже близко к ответу на первый вопрос о стоимости привлечения пользователя.

Но перед тем как пустится в эти рассуждения, стоит вспомнить, кто такой, этот средний пользователь. Мало ли, может, лучшие времена продукта давно позади.

Поэтому стоит посмотреть этот же график, но разбив по поколениям и посмотрев последние 24 поколения. Сразу же наложим сюда стоимость привлечения и разобьем по годам(чисто для удобства сравнения):

lk1[,':='( min_date=min(disbursement_date)),.(borrower_id)][,c("gen",'dif'):=.(floor_date(min_date,'1 month'),as.numeric(date-min_date))][,n:=uniqueN(borrower_id),.(gen)][,  .(sum=sum(am*0.00003),n=unique(n)),  .(gen,dif)# сгруппируем данные  по поколениями][order(gen,dif)][,":="(bal=bal<-sum/n)][,':='(cum=cumsum(bal),m_dif=max(dif)),.(gen)][dif<=m_dif-30 & gen %between% c('2018-01-01','2021-03-31')][, ggplot(.SD,aes(dif,cum,col=factor(gen)))+ geom_line()+ facet_wrap(~factor(year(gen),levels = c(2019,2018)),nrow=2)+tt+ff+ labs(x='дни жизни заемщика',y='доход в USD',col='поколение', title = 'LTV клиента по поколениям и по годам')+ scale_y_continuous(breaks = seq(-200,200,20), labels =paste0('$',seq(-200,200,20),'k' ))+ scale_x_continuous(breaks = seq(0,1000,20))+ geom_hline(yintercept = 695494*for_ex,color='red',size=1)+ geom_hline(yintercept = 0,color='dark red',linetype='dashed')+ geom_text(inherit.aes = F,aes(x=as.Date(600), y=695494*for_ex+3,group=1),label='Стоимость привлечения клиента = $20.86', col='red',size=6)+tt+ff+ theme(legend.text = element_text(size=20),       legend.title = element_text(size=25))+  guides(colour = guide_legend(override.aes = list(size=10)))]

Если каждая траектория - это поколение пользователей (месяц в котором выдан первый кредит), то мы можем сделать несколько важных выводов исходя из следующего:

  1. Точка, из которой начинаются линии - размер первого кредита.

  2. Чем выше траектория, тем больше денег на одного пользователя мы заработали в этом поколении.

  3. Чем раньше траектория поколения пересекает 0, тем быстрее мы начинаем зарабатывать на привлеченном пользователе.

  4. Чем раньше линия пересекает линию стоимости привлечения, тем раньше мы зарабатываем достаточно, чтобы окупать его в данном поколении.

Выводы

  1. Продукт становится лучше. Траектории более поздних поколений пересекают ноль и стоимость привлечения раньше, чем траектории старых поколений (150 дней против 180).

  2. Разница в темпах окупаемости объясняется скорее стартовой точкой и первой парой кредитов. Например, поколения конца 2018 года окупались лучше, имея меньшую сумму первого кредита и большую сумму второго. Это можно понять по провалу на траектории на 10-40 днях. Так же в 2019 году: поколения с меньшей средней суммой первого кредита окупали себя намного быстрее.

  3. На дистанции в один год мы заработаем на одном пользователе от 40 до 60 долларов. Максимальный размер заработка неизвестен. Ни одно поколение пока не вышло на плато, но экстраполируя, оценка в 120-150 долларов на дистанции в 3 года выглядит разумной

  4. Наиболее существенное влияние на траекторию оказывают события первых 90 дней, после этого темп роста плюс минус одинаковый.

Мы тут ради ответов, а не картинки смотреть

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

1. Сейчас мы платим 695494. Какая стоимость привлечения для нас является приемлемой? Имеет ли смысл повысить стоимость привлечения на клиента, чтобы получить больший объем?

Ситуация выглядит следующим образом - у нас есть успешный продукт, который мы не масштабируем - и это проблема. Ожидания по доходности от клиента на дистанции год - 40-60 долларов. Текущие затраты на привлечение $20.8 (695494* 0.00003)

В принципе - увеличение стоимости привлечения наша экономика выдержит, портфель не станет убыточным. Но в продукте - длинный срок окупаемости клиента.

Если мы можем позволить себе тратить сейчас - чтобы заработать потом - эксперименты оправданы.

2. Насколько здоровой выглядит экономика портфеля и какая динамика здесь и сейчас?

Портфель устойчиво положительный, с динамикой к росту за счет практик развития клиентов в первые дни жизни. Главное пространство для идей - механика работы с клиентами возраста 100+ дней.

Там существенных прорывов пока не было.

3. Мы недавно изменили подход к размеру выдачи и стали в первые кредиты выдавать меньшие чеки. Как это сказалось на продукте?

Вообще - видно что поколения в которых первый кредит был меньшим по размерам окупали себя чуть лучше. Четкий ответ - только через АБ тест.

Итог

Работа продукт менеджера это принимать решения. 2.2 млн строк рассказали о том, что происходит. Такой анализ занимает от 30 минут до пары часов, в зависимости от знания предметной области и грязи в данных. Для такого анализа не нужно ничего, кроме сырых данных и открытого ПО.

Несколько десятков строк кода, чуть здравых размышлений и экономика продукта ясна, посчитана и выводы сделаны.

Из этих же данных несложно собирается еще несколько важных оценок и выводов , но о них в дуговой раз.

Подробнее..

Как создать Trello dashboard, чтобы задачи из 5 досок собирались в одной?

16.05.2021 18:12:28 | Автор: admin

Проблематика

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

Какие есть варианты?

Сравнивая различные инструменты и таскменеджеры, выбор пал на одного из популярных игроков рынка - Trello. Однако, сразу после выбора программы, возникло новое препятствие. В бесплатной версии этого приложения отсутствовала возможность синхронизации досок нескольких сотрудников воедино без сторонних сервисов, типа Zapier, где для большого проекта тоже нужна лицензия.

В чем смысл объединения?

  1. Заходить в 5 досок и просматривать отдельно каждого, просто не хватит времени и сил.

  2. Если не синхронизировать доски на одном экране, будет крайне сложно сравнить одного сотрудника с другим по уровню текущей нагрузки.

  3. Сотрудники хотят понимать нагрузку каждого из членов команды, а для этого нужно опять же как то сводить все воедино.

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

Как синхронизировать доски 5 сотрудников в одной доске "дашборде" и при этом не оплачивать лицензию в Трелло или передаточных сервисах типа Zapier?

Решение задачи:

К счастью для работы с досками trello уже создан полноценный пакет на языке R, который позволяет подключаться к API таскменеджера, и именно им я и решил воспользоваться для решения всех задач.

Ход решения:

Сначала получим ключи и секретный код для работы с API https://trello.com/app-key (предварительно необходимо авторизоваться под своей учеткой в Trello и открыть себе доски сотрудников с правами администратора)

Сохраняем ключ и токен, который можно найти внизу этой же страницы

Далее установим сам язык R. Для его подключения нужноскачатьи установить инсталляторы. Также для комфортной работы с R рекомендуюиспользоватьбесплатную среду разработки RStudio.

Для решения вышеописанной задачи нам потребуется установить пакеты для:

  • работы с API Trello trelloR

  • работы со временем и временными периодами lubridate

  • работы с таблицами и агрегации данных dplyr

Чтобы установить пакеты из основного репозитория CRAN примените базовую функцию install.packages, а для загрузки пакетов с github функцию install_github:

remotes::install_github("jchrom/trelloR")install.packages("lubridate", dependencies = TRUE)install.packages("dplyr ", dependencies = TRUE)

Подключаемся к API и получаем токен:

# Указываем путь к папке куда будет сохранен полученный tokensetwd("C:\\*********\\R_script\\trello")# Получаем tokenmy_token = get_token("my_app", key = "", secret = "",                     expiration = c( "never"))

Полученные данные применяем для того, чтобы отправить первый запрос, и пишем функцию, которая поможет упростить наш код:

trelloadd <- function(delcard = NULL,                      addcard = NULL,                      nlista = NULL){  # Находим исходный лист сотрудника в Трелло откуда будем брать карточки  для записи в дашборд  ishod_tab <- get_list_cards(addcard)    # Определяем доску куда пишем новые карточки  bid = get_id_board(delcard)    # Получаем все списки с листами на доске дашборда  lid <- get_board_lists(bid)$id[nlista]    # Получаем все карточки в необходимом листе куда пишем  cid<-get_list_cards(lid)    # Удаляем карточки, которые уже есть в листе сотрудника на дашборде  if (length(cid$id)>0) {    for (i in 1:length(cid$id)) {      delete_resource(resource = "card", id = cid$id[i])    }  }else{    print("no-del")  }    # Находим дату создания карточки в исходном листе  dateList<- data.frame(dateadd = NA)  for (i in 1:length(ishod_tab$id)) {    cardID <- ishod_tab$id[i]    dateList[i,1] <- strtoi(strtrim(cardID, 8), 16L)  }  dateList$dateadd <-as.POSIXct(dateList$dateadd, origin = "1970-01-01")    # Циклом собираем лист записи и создаем карточку для переноса в дашборд  if (length(ishod_tab$name)>0) {    for (i in 1:length(ishod_tab$name)) {        payload = list(        idList = lid,        name = ishod_tab$name[i],        desc = paste0(ishod_tab$desc[i],"Date Add: " ,dateList$dateadd[i], "                     В работе уже ", floor(as.vector(difftime(today(),dateList$dateadd[i], units='days'))), " день"),        start = ishod_tab$start [i],        due = ishod_tab$due [i],        pos = "bottom"      )      r <- create_resource("card", body = payload)    }  }else{    print("Ok")  }  if (nrow(bind_rows(ishod_tab$labels[]))>0) {      # Добавляем лейблы (метки)    bid = get_id_board(delcard)    lid <- get_board_lists(bid)$id[nlista]    cid <-get_list_cards(lid)        # Находим карточки без меток    nlab <- which( lapply(ishod_tab$labels, length)!=0 %in% T)    for (i in nlab) {      labl <- ishod_tab$labels[[i]]      for (xi in 1:nrow(labl)) {        r <-  add_label(cid$id[i], color = ishod_tab$labels[[i]][xi,4],                        name = ishod_tab$labels[[i]][xi,3] )        }    }   }else{    print("no_lable")  }}

В функции необходимо будет только заполнять переменные:

  • delcard - Это id дашборда в который будет записываться информация из досок сотрудников

  • addcard - Это id листа из которого будут браться карточки для переноса в дашборд

  • nlista - номер листа в дашборде в который будут заноситься карточки

Получаем delcard

Для получения id дашборда куда будем писать общие карточки, сначала необходимо создать эту доску в самом Трелло, а затем из поисковой строки браузера скопировать необходимый нам id

Пример ссылки: https://trello.com/b/*********/общие-задачи - где значения ****** это будет Id конкретной доскиПример ссылки: https://trello.com/b/*********/общие-задачи - где значения ****** это будет Id конкретной доски

Получаем addcard

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

get_board_lists("https://trello.com/b/*****/сотрудник1", query = list(customFieldItems = "true"))

В результате выполнения функции, прямо в консоли R мы получим перечень всех листов на доске и по названию листа можем выбрать необходимый нам id для получения карточек

Данную процедуру мы проделываем для досок всех наших сотрудников из досок которых мы планируем забирать карточки для переноса на дашборд.

Далее проходим доски всех сотрудников и перезаписываем их на общий дашборд используя функцию созданную выше:

# Сотрудник1 ####trelloadd(delcard = "od*****W",           addcard = "600**********04",          nlista = 2)# Сотрудник2 ####trelloadd(delcard = "od*****W",           addcard = "5fc4********24",          nlista = 3)# Сотрудник3 ####trelloadd(delcard = "od*****W",           addcard = "5e94*********8ce",          nlista = 4)# Сотрудник4 ####trelloadd(delcard = "od*****W",          addcard = "5faa*********c522",          nlista = 5)# Сотрудник5 ####trelloadd(delcard = "od*****W",          addcard = "60744*******3394",          nlista = 6)# Сотрудник6 ####trelloadd(delcard = "od*****W",          addcard = "5e73******b07",          nlista = 7)

Так же я продумал вопрос, связанный с тем, чтобы получать в карточки дашборда информацию о том, когда была создана исходная карточка, чтобы можно было оценить, как долго уже решается та или иная задача (ведь дедлайны можно менять). Дата привязывается к моменту создания исходной карточки. Далее настраиваемавтоматический запуск скриптадля актуализации дашборда и задаём нужное количество раз обновлений в день.

Готовый дашбордГотовый дашборд

В итоге:

  1. Мы получили полноценную синхронизацию всех досок любого числа сотрудников в trello

  2. Решили проблему с тем, чтобы наглядно иметь доступ к дате создания каждой задачи

  3. Имеем фундамент для анализа продуктивности сотрудников в любых разрезах, благодаря возможности добавлять в код новые различные улучшения расширяя код на основе Api от Trello, на основе бизнес логики вашей компании и возможностей языка R.

Подробнее..

Модификация EM-алгоритма для решения задачи кластеризации с выбросами

05.06.2021 12:14:00 | Автор: admin

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

Один из подходов к решению данной задачи (чтобы метод кластеризации автоматически отсеивал выбросы) получил название "optimally tuned robust improper maximum likelihood estimator" и был описан вот в этой статье 2017 года (http://dx.doi.org/10.1080/01621459.2015.1100996), а недавно и получил реализацию на R. Поговорим о нем.

Минутка теории

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

Математический апгрейд классического алгоритма состоит в том, что берется чуть иное предположение. Считается, что функция вероятности распределения точек является суммой смеси гауссиан и равномерного распределения шумовых точек (считается, что выбросы распределены равномерно), то есть

 - несобственная постоянная плотность (плотность шума) - несобственная постоянная плотность (плотность шума)

Это приводит к некоторому усложнению решения задачи оптимизации, которая теперь выглядит как решение задачи на максимум

но вот с такими ограничениями

Эта задача оптимизации (особенно с условиями, накладываемыми на отношение собственных значений) далеко не всегда имеет решение. Авторы честно об этом говорят, но также они утверждают, что их алгоритм достаточно спокойно справляется к кластерами довольно сложной формы. Проверим

Эксперимент

Создадим какой-нибудь двумерный датасет, в котором форма кластеров будет не особо отличаться от нормальных.

library(ggplot2)# Создаем базу данныхset.seed(739)n <- 500 # numer of pointsy <- abs(c(rnorm(n,5,2)))x <- c(rnorm(n,5,1))/sqrt(y)class<-rep(1,n)dat1 <- as.data.frame(cbind(x,y,class)) # Первый кластер - что-то типа кривоугольного прямоугольного треугольникаy <- c(rnorm(n,8,0.4))x <- rlnorm(n, meanlog = log(7), sdlog = 0.2) class<-rep(2,n)dat2 <- as.data.frame(cbind(x,y,class)) # Второй кластер - больше похож на форме на горизонтальный прямоугольникy <- c(runif(n, min = 2, max = 7))x <- c(rnorm(n,8,0.4))class<-rep(3,n)dat3 <- as.data.frame(cbind(x,y,class)) # Третий кластер - вертикальный прямоугольникy <- c(runif(n/10, min = 0, max = 10))x <- c(runif(n/10, min = 0, max = 10))class<-rep(0,n/10)dat4 <- as.data.frame(cbind(x,y,class)) # Шумdat <- rbind(dat1,dat2,dat3,dat4)colnames(dat) <- c("x","y", "class")dat$class <- as.factor(dat$class)dat_test <- as.data.frame(dat)p_base <- ggplot(dat,aes(x=x,y=y,color=class)) + geom_point()ggExtra::ggMarginal(p_base, groupColour = TRUE, groupFill = TRUE)

Получаем вот такую картинку

Здесь и далее меткой "0" обозначаются наблюдения, определенные как шумЗдесь и далее меткой "0" обозначаются наблюдения, определенные как шум

Пакет otrimle достаточно современен в том плане, что в нем уже есть функция, позволяющая сравнить результаты кластеризации в случае задания нескольких кластеров. Зададим число кластеров от 2 до 5, и попробуем посмотреть на ход выполнения.

library(otrimle)clus <- otrimleg(dat[,c(1,2)], G=2:5, monitor=1) # параметр monitor позволяет видеть ход выполнения

В результате выведется вот что-то такое

В теории, этого уже достаточно для определения оптимального числа кластеров (мы же решаем задачу на максимум, и, в теории, нужно взять число кластеров, для которого iloglik максимален. С другой же стороны, видно, что значимого преимущества в разбиении на 4 или 5 кластеров по сравнению с 3 нет - поэтому выбор количества кластеров в значительной степени эмпирика). Но желательно посмотреть еще и на количество наблюдений внутри кластеров. Поэтому, не отходя далеко, жмем

clus$solution

и получаем следующую информацию

Для каждого из построенных разбиений Noise - количество элементов, классифицированных как выбросы. Совсем аномальных кластеров (в которых 1,2,10 наблюдений) вроде бы нет - поэтому более подробно посмотрим на разбиение с 5 кластерами.

clus_2 <-otrimle(dat[,c(1,2)], G = 5, npr.max = 0.01, erc = 20, monitor = TRUE)# npr.max - максимальная доля выбросов в выборке# erc - соотношение максимального/минимального собственных значенияclus_2$codeclus_2$flag

Если clus_2$code возвращает 0, это значит, что задача оптимизации вообще не решилась, если clus_2$code = 1, это означает, что EM-алгоритм не сошелся (и надо поиграться с параметрами), если clus_2$code = 2, то все хорошо, алгоритм все посчитал.

Параметр clus_2$flag дает информацию о внутренней структуре работы алгоритма. Если

clus_2$flag = 1, то присутствует вырождение апостерирорных вероятностей

Фактически, это говорит о наличии лишних кластеров, так как вероятность одного из компонент смеси близка к 0.

clus_2$flag = 2, то для сходимости алгоритма было нарушено предположение о доли шума

clus_2$flag = 3, то для при расчетах ограничение на долю шума было использовано

clus_2$flag = 4, то для при расчетах было использовано ограничение на отношение собственных значений

В нашем случае все хорошо (clus_2$code = 2, clus_2$flag = 4), продолжаем разбираться дальше.

clus_2$mean # центра кластеровhead(clus_2$tau) # вероятности принадлежности к кластерамhead(clus_2$cluster) # принадлежность к кластерам

Тут мы можем посмотреть центра кластеров и вероятности отнесения наблюдений к шуму или иным кластерам. Теперь еще посмотрим на то, как алгоритм классифицировал наши наблюдения.

Слева - истинное разбиение, справа - предложенное алгоритмом для случая 5 кластеровСлева - истинное разбиение, справа - предложенное алгоритмом для случая 5 кластеровСлева - истинное разбиение, справа - предложенное алгоритмом для случая 4 кластеровСлева - истинное разбиение, справа - предложенное алгоритмом для случая 4 кластеровСлева - истинное разбиение, справа - предложенное алгоритмом для случая 3 кластеровСлева - истинное разбиение, справа - предложенное алгоритмом для случая 3 кластеровСлева - истинное разбиение, справа - предложенное алгоритмом для случая 2 кластеровСлева - истинное разбиение, справа - предложенное алгоритмом для случая 2 кластеров

Можно заметить, что даже если угадать с числом кластеров (3), то результаты кластеризации, вообще-то, очень сильно будут отличаться от истинных - и более адекватную картину будут давать разбиения с большим числом кластеров, чем на самом деле. Это произошло из-за отличной от эллиптической формы кластеров, но в целом алгоритм работает неплохо даже в условиях шума.

Подробнее..
Категории: R , Кластеризация

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru