Раздел 42 · Аналитика данных с нуля

Мониторинг метрик: алерт по скользящему z, день недели, ложные тревоги и тишина

middle~15 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Бран · продюсер «Хроник Пограничья»

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

В уроке про DAU мы нашли бот-ферму farm…x вручную: четыре аккаунта, которые сидели в игре почти круглые сутки. Бран просит, чтобы такое находил запрос, сам и каждый день. Это алерт. В прошлом уроке мы научились отличать сезон от тренда. Сегодня научим запрос отличать нормальный день от ненормального.

Метрика для алерта: прохождения квестов за день, quest_runs. Боты проходят квесты сотнями, так что авария должна быть видна.

Пожарная сигнализация

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

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

Правило: среднее плюс-минус k отклонений

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

WITH daily AS (
  SELECT started_at::date AS day, count(*) AS runs
  FROM quest_runs
  GROUP BY 1
), stats AS (
  SELECT day, runs,
         avg(runs) OVER w AS mean_28,
         stddev_samp(runs) OVER w AS sd_28,
         count(*) OVER w AS history
  FROM daily
  WINDOW w AS (ORDER BY day ROWS BETWEEN 28 PRECEDING AND 1 PRECEDING)
)
SELECT day, runs, round(mean_28, 1) AS mean_28, round(sd_28, 1) AS sd_28,
       round((runs - mean_28) / sd_28, 2) AS z
FROM stats
WHERE history = 28 AND abs(runs - mean_28) > 3 * sd_28
ORDER BY day;

Главное в запросе это рамка 28 PRECEDING AND 1 PRECEDING: в окно не входит сам проверяемый день. Разницу видно на цифрах. С честной рамкой 2 апреля даёт z = 6,78. Если взять 27 PRECEDING AND CURRENT ROW, выброс сам себе поднимает среднее и раздувает отклонение, и z падает до 4,13. У такого окна есть потолок: день, который входит в своё окно из 28 строк, не может получить z больше 27 / sqrt(28), то есть 5,1, какой бы ни была авария. Алерт сравнивает день с прошлым, как и в жизни: утром ты знаешь только вчерашние дни.

history = 28 выкидывает первые четыре недели игры: по пяти точкам отклонение ничего не значит.

 day        | runs | mean_28 | sd_28 |  z
------------+------+---------+-------+------
 2026-02-21 |  211 |   131.6 |  22.1 | 3.59
 2026-04-02 |  349 |   184.0 |  24.3 | 6.78
 2026-04-03 |  523 |   191.6 |  38.1 | 8.69
 2026-04-04 |  718 |   204.6 |  72.8 | 7.05
 2026-04-05 |  972 |   223.4 | 121.2 | 6.18
 2026-04-06 | 1014 |   251.8 | 185.8 | 4.10

Бот-ферма поймана в первый же день, 2 апреля. Брану пришло бы письмо утром 3-го. Но в списке есть два изъяна.

Изъян 1: алерт слепнет

Боты сидели до 14 апреля, а тревоги кончились 6-го. 7 апреля было 978 прохождений, впятеро больше нормы, а z всего 2,97. Аномальные дни сами вошли в окно: среднее выросло до 282, отклонение до 234. Через неделю аварии алерт считает её нормой.

Три способа лечить, от простого к сложному:

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

урок из курса

Урок из курса «Аналитика данных с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.

Оплата пока не подключена: премиум и курсы открываются промокодом.