Обнаружение и использование тенденций данных в программных системах

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

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

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

вступление

В качестве основной функции в PandaDoc у нас есть редактор документов, который позволяет пользователям редактировать документы в режиме реального времени. У многих конкурентов есть возможность загрузить PDF-документ и поместить в него настраиваемые поля, но великая сила шаблонов заключается в том, что вы можете изменять все, что добавляете в свой шаблон — текст, мультимедиа, ссылки, поля и т. д. Это то, что предлагает наш редактор. хорош в том, что вы можете ввести любой текст, оставить местозаполнитель, чтобы автоматически заполнить его данными вашего клиента, настроить стиль документа, рассчитать свои услуги и многие другие интересные функции.

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

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

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

Учитывая ввод

Формализуем имеющиеся данные. У нас есть последовательность записей (я буду называть их «событиями»), представляющая каждое действие пользователя. Такая последовательность является последовательностью «только для добавления» — нет возможности удалить или изменить существующие элементы. События хранятся в таблице базы данных (фактическое хранилище с указанными выше характеристиками большой роли не играет, но в нашем случае это была таблица Postgres) под названием «журнал событий» с автоинкрементируемым первичным ключом. Каждое событие имеет следующую информацию:

  • timestamp - когда произошло событие (предположим, что это значение не по убыванию),
  • metainfo - Меня это не особо заботило, но это может быть любое имя, параметры, id пользователя и так далее.

Я собираюсь ссылаться на таблицу журнала событий следующим образом:

Сбор данных

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

В зависимости от объема данных вы можете выбрать два возможных варианта сбора ваших данных:

  1. Последовательно просмотрите всю таблицу и сгруппируйте каждую запись в соответствии с ее timestamp. Очевидно, что сложность задачи O(N), где N — количество записей в базе данных. Результаты будут очень точными.
  2. Для каждого дня исторических данных найдите первую и последнюю запись (используя timestamp) за этот день. Это можно сделать за время O(D*log(N)), где:
  • D — количество дней, за которые вы хотите агрегировать данные,
  • N — это количество записей в базе данных.

Последний случай основан на двух важных допущениях:

  1. временные метки событий уменьшаются достаточно, т.е. могут быть локальные нарушения инварианта, например, если временная метка отражает время прикладного уровня, но они мало влияют на результат,
  2. и количество событий может быть определено как last_event.id - first_event.id + 1. Это означает, что в значениях id нет «дыр» (каждый идентификатор события равен количеству записей в этой точке) или они существуют, но мы можем их игнорировать. Например, в реальном случае относительная ошибка составила около 0,1% (о том, что объем id значений отсутствовал в записях, а event.id отличалось на 0,1% от реального количества записей), которым я пренебрегал. Отсутствующие значения id могут появиться, например, когда транзакция INSERT прерывается — базовая автоматически увеличивающаяся последовательность обновляется, но запись окончательно не записывается.

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

Визуализация данных

Давайте просто воспользуемся фреймворком панды и создадим простой сюжет:

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

Что мы можем здесь увидеть?

  1. Последние десятилетия выглядят «ребристыми» — если сравнить даты, то можно заметить, что это соответствует еженедельному распределению событий — активное использование в рабочие дни и гораздо меньшая нагрузка в выходные.
  2. Пара «в гору», вероятно, может означать успешные маркетинговые кампании или большую нагрузку из-за потребностей бизнеса.

Круто, попробуем продолжить линию.

Построение прогнозной модели

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

Функция выглядит как простая геометрическая формула:

f(x) = A * x^B

Для подбора нужных коэффициентов можно решить задачу простой линейной регрессии (проще говоря — алгоритм позволяет найти коэффициенты следующей функции: f(x1, x2, x3, ..., xn) = C + W1 * x1 + W2 * x2 + ... + Wn * xn или f(x) = C + W*x в векторном виде).

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

f(x)     = A * x^B; 
ln(f(x)) = ln(A * x^B); 
ln(f(x)) = ln(A) + ln(x^B); 
ln(f(x)) = ln(A) + B * ln(x).  
---->> g(x') = A' + B' * x' <<------

И в итоге получаем вот такую ​​картинку (оценка показывает нам, насколько хороша аппроксимация: чем ближе значение к 1,0 — тем оно совершеннее):

Score 0.928 
f(x) = 178.728 * x^2.04, x - id(day), f(x) - #events in total

Что на самом деле… не то, что мы ожидали.

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

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

... 
X_, y_ = np.log(X)[-100:], np.log(y)[-100:] 
...

Что производит следующий вывод:

Score 0.999 
f(x) = 0.001 * x^3.997, x - id(day), f(x) - #events in total

Намного лучше.

С помощью такой функции мы теперь можем прогнозировать количество записей в журнале событий — например. удвоение числа произойдет к концу года (2020 г.), а первый миллиард записей — летом 2021 г. Или вы можете узнать, когда вы достигнете максимального целочисленного значения в виде записи таблицы id и вам нужно мигрировать ее некоторым bigint. Или, зная текущий размер базы данных, мы можем спрогнозировать необходимое дисковое пространство через год, и, возможно, к этому времени нам нужно будет разбить таблицу журнала событий (например, путем разбиения).

Может быть, что-то еще полезное мы можем извлечь из этих данных? Я уверен, что мы можем.

Рост событий

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

f(x) = 0.0011575004 * x^3.9969877517; 
f'(x) = 0.0011575004 * 3.9969877517 * x^(3.9969877517 - 1); 
f'(x) = 0.0046 * x^2.997;

На момент наличия около 250 млн событий база данных ежедневно хранила около 1,4 млн событий по этой формуле. Что составляет около 1k событий в минуту. С помощью таких расчетов вы можете планировать свои ресурсы с точки зрения сервисных машин или виртуальных машин, шлюза RPS, статистики одновременных подключений к базе данных и т. д.

Рост пользователей

Отлично, до сих пор мы пытались оценить требуемые вычислительные ресурсы.

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

Если бы нас попросили описать формулу количества событий в журнале событий в конкретный день, на какие параметры мы бы опирались? Первая идея, которая приходит на ум, — опираться на результаты предыдущего дня и вывести рекуррентную формулу:

f(x) = f(x - 1) + e(x)

где e(x) количество событий, добавленных в x день.

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

С другой стороны, наш бизнес растет. А наша маркетинговая кампания позволяет нам регистрировать новых пользователей. Давайте представим, что наша пользовательская база растет на постоянное число каждый день (потом мы решим, достаточно ли этого) — на U.

Теперь формула активных пользователей в данный день будет выглядеть так:

u(x) = u(x - 1) + U = U * x

И объем событий в журнале событий будет следующим:

e(x) = u(x) * E 
 
f(x) = f(x - 1) + e(x);
f(x) = f(x - 1) + u(x) * E;
f(x) = f(x - 1) + U * E * x;
f(x) = U * E * [x + (x - 1) + (x - 2) + (x - 3) + ... + 1];
f(x) = U * E * [x * (x + 1) / 2];
f(x) ~ x^2

В итоге мы получили важный результат — количество событий в журнале событий растет квадратично (с точки зрения асимптотической эквивалентности)!

Если мы посмотрим на график зависимостей, наша функция f(x) зависит от e(x) (среднее количество пользовательских правок в день), а сама e(x) зависит от u(x) (количества активных пользователей). Таким образом, единственным «фактором влияния» целевой формулы является количество активных пользователей или наша пользовательская база. В приведенном выше примере мы предположили, что пользовательская база растет линейно (u(x) ~ x). Но что, если мы поменяем его на что-то более агрессивное?

u(x) ~ x^2;
e(x) = u(x) * E;
e(x) ~ x^2;
f(x) = f(x - 1) + e(x);
f(x) ~ f(x - 1) + x^2;
f(x) ~ x^2 + (x - 1)^2 + (x - 2)^2 + ... + 1^2;
f(x) ~ x^3

Разматывая результат и используя известные формулы суммирования целых степеней, приходим к выводу, что функция роста пользователей асимптотически эквивалентна x³!

u(x) ~ x^3;
e(x) = u(x) * E;
e(x) ~ x^3;
f(x) = f(x - 1) + e(x);
f(x) ~ f(x - 1) + x^3;
f(x) ~ x^3 + (x - 1)^3 + (x - 2)^3 + ... + 1^3;
f(x) ~ x^4

Эпилог

Важно знать, что чем дальше вы уходите от исходных данных (и чем больше вы полагаетесь на моделирование), тем менее точным становится ваш результат. В сценарии роста пользователей мы не столкнулись, например, с оттоком пользователей и автообновлениями (или «Smart Content», когда документ может меняться автоматически в зависимости от каких-то триггеров). Вы должны найти компромисс между «чрезмерным моделированием» и «недооценкой». Но даже несмотря на то, что такие наблюдения не дают точных цифр, что более важно, они позволяют увидеть динамику использования системы и то, насколько хорошо идет бизнес.

О платформе API PandaDoc:

PandaDoc — это простое и масштабируемое все-в-одном API-решение для создания документов и захвата электронной подписи путем их встраивания в ваше приложение, веб-сайт или с помощью серверного API. У нас есть подробная документация, поэтому вы можете легко начать работу. Если вам нужна помощь или нужна экспертиза решения вашей бизнес-задачи, пишите нам сюда.