Стек: Python, Airflow, ClickHouse, Slack
В iGaming падение активности игровых провайдеров почти никогда не выглядит как "обрыв". Чаще это медленное затухание: ставок становится меньше, затем еще меньше, игроки уходят постепенно. Формально провайдер продолжает работать, стандартный мониторинг молчит, а бизнес уже теряет деньги.
Моя задача была не фиксировать факт полного падения активности, а поймать момент, когда траектория уже направлена вниз, но ситуацию ещё можно развернуть.
Вся логика работает внутри DAG в Airflow. Он запускается каждые 6 минут и анализирует активность игровых провайдеров в реальном времени. При обнаружении начала падения или восстановления система отправляет уведомления в Slack.
with DAG(
dag_id="turnovers_stats_upd",
schedule="*/6 * * * *",
dagrun_timeout=timedelta(minutes=10),
tags=["alert"],
) as dag:
Алгоритм
Исходные данные - это поток игровых событий (ставок), поступающий от провайдеров. В сыром виде для мониторинга они почти бесполезны: данные шумные, с резкими всплесками и провалами, на фоне которых реальный сигнал теряется.
Поэтому первым шагом я агрегирую события в поминутное количество ставок для каждого провайдера. Важно, что в итоговом временном ряду я учитываю не только минуты, в которые ставки были, но и минуты с их полным отсутствием.
select
d.Merchant,
d.TimeMin,
COALESCE(b.BetCount, 0) AS BetCount
from dates d
left join bets b on d.Merchant = b.Merchant
and d.TimeMin = b.TimeMin
Даже после агрегации поминутные данные остаются слишком шумными. Одиночные пустые минуты, задержки событий или случайные всплески легко вызывают ложные срабатывания.
Я перепробовала разные варианты сглаживания данных:
минутное окно - слишком шумно;
5 минут - реагирует поздно;
10 минут - пропускает начало снижение активности.
На практике хорошим компромиссом оказалось окно в 3 минуты. Оно сглаживает случайные колебания, но при этом не вносит заметной задержки в обнаружение проблем.
df["grp_3m"] = (
df.sort_values("TimeMin")
.groupby("Merchant")
.cumcount() // 3
)
df_3m = (
df.groupby(["Merchant", "grp_3m"])
.agg(BetCount=("BetCount", "sum"))
.reset_index()
)
В дальнейшем весь анализ строится уже на этих трехминутных точках
Адаптивная граница "нормальной" активности
Следующим шагом стало определение нормального уровня активности для каждого провайдера. Сначала я пробовала классические пороги - например, "если ставок меньше X, шлём алерт". Это вообще не сработало: один провайдер шумный, другой стабильный, третий живет по расписанию матчей. Порог приходилось крутить вручную, и он все равно ломался, поэтому я использовала базовую статистику - среднее количество ставок и стандартное отклонение, рассчитанные на сгруппированных данных.
Среднее значение показывает привычный уровень активности провайдера, а стандартное отклонение - насколько сильно эта активность обычно колеблется. Это позволяет отличать реальное падение активности от нормального шума.
При этом для разных частей алгоритма используются разные временные окна. Граница нормальной активности рассчитывается на расширенном интервале данных, чтобы учитывать типичное поведение провайдера на более долгом периоде, сам анализ тренда (описанный далее) выполняется на коротком окне из последних трёхминутных интервалов и отражает текущее направление движения активности. Такое разделение позволяет одновременно учитывать как долгосрочную норму, так и краткосрочную динамику
Здесь - количество ставок в
-м временном интервале,
- число интервалов
bc_mean = df_3m["BetCount"].mean()bc_std = df_3m["BetCount"].std(ddof=0)
На основе этих значений я задаю нижнюю допустимую границу активности:
Таким образом, нижняя граница автоматически адаптируется под поведение конкретного провайдера: чем более стабильна активность, тем чувствительнее система к падениям.
Коэффициент k подбирался эмпирически. Он позволяет учитывать, насколько шумной обычно бывает активность конкретного провайдера.
lower_bound = bc_mean - 1.5 * bc_std
Однако одного сравнения с границей недостаточно. Активность может временно просесть и тут же вернуться. Поэтому я смотрю не только на абсолютное значение показателя, но и на направление движения.
Для этого я беру последние 7 трехминутных точек и строю по ним линейный тренд. Меня интересует не столько текущее значение, сколько знак и величина наклона.
Количество точек для тренда и горизонт прогноза подбирались эмпирически. Меньшие значения давали слишком шумный тренд, большие начинали запаздывать с реакцией. Связка из последних 7 трехминутных точек и прогноза на 5 точек показала наиболее стабильное поведение на проде.
y = last_7["BetCount"].valuesx = np.arange(len(y))a, b = np.polyfit(x, y, 1)
Дальше я смотрю не на текущее значение, а на то, куда активность движется. По последним точкам я строю линейный тренд и делаю короткий прогноз вперед. Если тренд устойчиво отрицательный и прогноз уходит к нулю, при том что рассчитанная нижняя граница все еще остается выше нуля, я считаю это признаком того, что просадка не случайная и, скорее всего, будет продолжаться
future_x = np.arange(len(y), len(y) + 5)y_pred = a * future_x + b
В этот момент провайдер помечается как проблемный, и отправляется алерт о начале падения активности
Важно, что DAG не спамит уведомлениями. Для каждого падения отправляется ровно два сообщения: одно - в момент начала падения, второе - при восстановлении. Восстановление фиксируется, когда в течение нескольких окон подряд активность снова становится положительной и тренд меняет знак на положительный.
Алгоритм в первую очередь нацелен на раннее обнаружение плавных падений активности, при этом резкие обрывы активности также попадают в его зону видимости

Результаты
После реализации я сначала прогнала алгоритм на исторических данных, а затем вывела в прод. В качестве базовой метрики я использовала долю падений активности провайдеров, которые система смогла обнаружить и корректно заалертить.
До этого использовался базовый мониторинг на фиксированных порогах и фактическом отсутствии ставок. Он срабатывал примерно в 58% случаев и, как правило, уже тогда, когда активность почти полностью исчезала.
Текущая версия DAG, основанная на сглаживании, адаптивных границах и анализе тренда, корректно обнаруживает около 93% падений активности провайдеров, включая плавные и растянутые во времени деградации, которые ранее оставались незамеченными. При этом количество ложных срабатываний удалось удержать в разумных пределах за счет анализа направления тренда и логики фиксации восстановления.
