«Мы сделаем вас счастливыми! Вы будете счастливыми!»
«Отроки во Вселенной» (1974)


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


Аллегория вполне понятная, пытаться объяснять что-либо — в 99% случаев процедура бессмысленная и энергозатратная. Поэтому ниже пример, как двумя экранами кода можно быстренько превратить временные ряды, которые почти всегда встречаются в больших количествах, в светофор.


Все предыдущие публикации.


Приступаем к процессу «осчастливливания».
Потребуются клей, бумага, скотч, и немного R.


Обычно временные ряды проглядывают на глубину 2-4 недель. Рядов может быть много, ОЧЕНЬ много и их надо как-то привести к светофорной раскраске.


Воспользуемся фактом небольшой глубины и соберем на коленке пульверизатор на основе линейной регрессии. Коэффициент $k$ нам определит цвет краски (красный/зеленый), а стат. значимость даст необходимый желтый для нестабильного поведения. Поскольку пишем на R, то никаких циклов. Базовую идею подсматриваем в книге «R for Data Science», глава «Many models».


Подключаем пакеты
library(tidyverse)
library(lubridate)
library(glue)
library(scales)
library(stringi)
library(jsonlite)
library(RColorBrewer)
library(extrafont)
library(hrbrthemes)
library(DBI)
library(anytime)
library(tictoc)
library(checkmate)
library(data.table)
library(ggplot2)
library(ggthemes)
library(cli)
library(gt)
library(lme4) # modeling

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


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


Сформируем каркасы 12 функций.


set.seed(12)
raw_df <-  1:12 %>%
  tibble(id = ., 
         k = (id - 1) %/% 4 - 1,
         b = runif(length(.), 10, 30)) %>%
  rowwise() %>%
  mutate(data = list(
    tibble(x = 0:13, y = k * runif(1, 0, 3)*x + b))
    )


Сформируем датасет в именах и значениях реальных данных.


signal_tbl <- raw_df %>%
  unnest(data) %>%
  mutate(date_msk = today() + x, 
         value = y + rnorm(nrow(.), sd = 3.5)) %>%
  mutate_at("id", as.factor)

signal_tbl %>%
  ggplot(aes(date_msk, value, group = id)) +
  geom_point() +
  facet_wrap(~id, ncol = 4)


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


OLS [код]
lm_tbl <- signal_tbl %>%
  select(id, date_msk, value) %>%
  nest(-id) %>%
  mutate(model = map(data, ~lm(value ~ date_msk, data = .x))) %>%
  mutate(m_params = map(model, broom::tidy)) %>%
  unnest(m_params) %>%
  filter(term == "date_msk") %>%
  setDT() %>%
  .[, id_trend := fifelse(estimate < 0, 
                          "Падение показателя", 
                          "Рост показателя")] %>%
  .[p.value > 0.05, id_trend := "Достоверно неизвестно"] %>%
  as_tibble()

Рисуем внятную табличку с большими буквами.


Табличка gt [код]
lm_tbl %>%
  select(id, estimate, std.error, p.value, id_trend) %>%
  mutate(grp = p.value < 0.05) %>%
  arrange(desc(grp), desc(estimate)) %>%
  gt() %>%
  cols_hide(columns = grp) %>%
  fmt_number(
    columns = p.value,
    decimals = 4,
    use_seps = FALSE
  ) %>%
  data_color(
    c(estimate),
    colors = scales::col_numeric(
      c("#f87274", "white", "#63be7b"),
      na.color = "transparent",
      domain = NULL
    )
  ) %>%
  # снимем раскраску с незначимых строк
  tab_style(
    style = list(
      cell_fill(color = "white"),
      cell_text(color = "black")
      ),
    locations = cells_body(rows = p.value > 0.05)
  )


Строим финальную картинку, согласуем цвета с корпоративным брендбуком.


Светографики [код]
lm_tbl %>%
  select(id, data, estimate, id_trend) %>%
  unnest(data) %>%
  ggplot(aes(date_msk, value, colour = id_trend, group = id)) +
  geom_point(alpha = 0.8) +
  ggthemes::scale_colour_tableau() +
  geom_smooth(se = FALSE, alpha = 0.5) +
  scale_x_date(labels = scales::date_format("%d.%m"), 
               breaks = scales::date_breaks("2 days"),
               guide = guide_axis(n.dodge = 2)) +
  scale_y_continuous(labels = scales::label_percent(accuracy = 1),
                     breaks = scales::breaks_pretty(n = 3)) +
  facet_wrap(~id, ncol = 3, scales = "free") +
  theme_ipsum_rc() +
  labs(x = "Дата", y = "Показатель", colour = "Динамика")


Собственно говоря, все.


Предыдущая публикация — «Важно ли DS аналитику знать про software development?»

Комментарии (2)


  1. Ad_fesha
    24.01.2023 10:50
    +1

    Илья спасибо за статью!

    Позволю себе чуть дополнить вариант с раскраской (вдруг кому пригодится)

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

    Пример https://r-analytics.blogspot.com/2019/12/feasts.html


  1. i_shutov Автор
    24.01.2023 10:54
    +2

    Михаил, спасибо за добавление.

    https://tidyverts.org/ вообще сам по себе очень полезен.