Présentation pour le DAMM de novembre 2022 par Sébastien Brodeur de Desjardins.
Il est possible d'utiliser BigQuery ML et un modèle prédictif ARIMA+ afin d'effectuer des prédictions sur des séries chronologique.
Ensuite, en comparant cette prédiction avec les valeurs réelles, il est possible d'identifier des anomalies potentielles.
Je vais donc démontrer une méthode simple d'effectuer une détection d'anomalies sur les données de Google Analytics 4 (GA4) exportée vers BigQuery un utilisant simplement quelques requêtes SQL. Oui oui, juste avec du SQL.
Voici les grandes lignes :
Comme tout projet de ML, l'étape la plus importante (et parfois la plus fastidieuse) est de préparer les données. Cette étape est pourtant la plus importante. Voici comment préparer vos données afin d'être prêt à être consommé par BigQuey ML pour effectuer des prédictions.
La table devra avoir le format suivant :
ts | dimension | value |
---|---|---|
2022-10-27 04:00:00 UTC | login | 100 |
2022-10-28 04:00:00 UTC | login | 130 |
2022-10-28 04:00:00 UTC | unique_user | 90 |
... | ... | ... |
Ne vous inquiétez pas, nous allons expliquer ces colonnes plus bas.
L'avantage de cette approche est que si vous désirez surveiller une nouvelle dimension/métrique, vous n'avez rien d'autre à faire que de modifier la requête qui génère cette table.
Nous allons d'abord créer une table avec les valeurs (dimensions/values) que nous souhaitons surveiller.
(Remplacez dans la requête PROJET et DATASET par les vôtres.)
CREATE OR REPLACE TABLE `PROJET.DATASET.detection_anomalies_data` AS (
# Requête pour surveiller le nombre event par jour pour certain event GA4.
SELECT
PARSE_TIMESTAMP("%Y%m%d", event_date, "America/Montreal") AS ts,
event_name AS dimension,
COUNT(*) AS value
FROM `PROJET.DATASET.events_*`
WHERE
_TABLE_SUFFIX >= FORMAT_DATE("%Y%m%d", DATE_SUB(CURRENT_DATE(), INTERVAL 120 DAY))
AND event_name IN ("screen_view", "search", "login", "purchase")
GROUP BY ts, dimension
UNION ALL
# Requête pour surveiller le nombre event par jour et par plateforme pour certain event GA4.
SELECT
PARSE_TIMESTAMP("%Y%m%d", event_date, "America/Montreal") AS ts,
CONCAT(event_name, " - ", platform) AS dimension,
COUNT(*) AS value
FROM `PROJET.DATASET.events_*`
WHERE
_TABLE_SUFFIX >= FORMAT_DATE("%Y%m%d", DATE_SUB(CURRENT_DATE(), INTERVAL 120 DAY))
AND event_name IN ("screen_view", "search", "login", "purchase")
GROUP BY ts, dimension
UNION ALL
# Requête pour surveiller le nombre event par jour et par type appareil pour certain event GA4.
SELECT
PARSE_TIMESTAMP("%Y%m%d", event_date, "America/Montreal") AS ts,
CONCAT(event_name, " - ", device.category) AS dimension,
COUNT(*) AS value
FROM `PROJET.DATASET.events_*`
WHERE
_TABLE_SUFFIX >= FORMAT_DATE("%Y%m%d", DATE_SUB(CURRENT_DATE(), INTERVAL 120 DAY))
AND event_name IN ("screen_view", "search", "login", "purchase")
GROUP BY ts, dimension
UNION ALL
# Requête pour surveiller le nombre unique de "visiteurs" (user_pseudo_id).
SELECT
PARSE_TIMESTAMP("%Y%m%d", event_date, "America/Montreal") AS ts,
"unique_user" AS dimension,
COUNT(DISTINCT user_pseudo_id) AS value
FROM `PROJET.DATASET.events_*`
WHERE
_TABLE_SUFFIX >= FORMAT_DATE("%Y%m%d", DATE_SUB(CURRENT_DATE(), INTERVAL 120 DAY))
GROUP BY ts, dimension
)
Ces 4 requêtes peuvent faire peur si vous n'êtes pas habitué à BigQuery, mais nous allons prendre le temps de les décortiquer. (Cliquer ici pour en savoir plus sur le schéma de données GA4 dans BigQuery.)
Pour fonctionner, le modèle ARIMA+ de BigQueryML s'attend à recevoir une colonne contenant un champ de type TIMESTAMP
. Comme nous allons effectuer une prédiction sur un total par jour, le champ event_date
(STRING
) de GA4 doit être convertis en TIMESTAMP
. C'est ce que nous faisons ici : PARSE_TIMESTAMP("%Y%m%d", event_date, "America/Montreal") AS ts
.
C'est ici que nous ajoutons ce que nous souhaitons surveiller. Il est possible d'utiliser UNION ALL
afin de combiner plusieurs requêtes ensembles. Dans l'exemple ci-haut, je crée dynamiquement plusieurs dimensions en combinant plusieurs champs de la table GA4.
- event_name - Avoir un décompte du nombre de fois que chaque event c'est produit (
COUNT(*) AS value
). Ici, afin de limiter le nombre de dimensions, j'utilise une condition dans la requête afin de sélectionner certains event_name seulement (WHERE event_name IN ("screen_view", "search", "login", "purchase")
) - event_name - PLATFORM - Ici je combine le event_name avec la platform (IOS, ANDROID, WEB) afin d'être en mesure de détecter un problème spécifique sur une plateforme précise. Par exemple, nous avons peut-être uniquement un problème sur IOS mais pas ANDROID. J'effectue la combinaison de 2 champs BigQuery avec un
CONCAT(event_name, " - ", platform)
. - event_name - DEVICE CATEGORY - Ici je combine le event_name avec le
device.category
(DESKTOP, MOBILE, TABLET) afin d'être en mesure de détecter un problème spécifique sur un type d'appareil. Par exemple, nous avons peut-être uniquement un problème sur le mobile. J'effectue la combinaison de 2 champs BigQuery avec unCONCAT(event_name, " - ", device.category)
. - unique_user - Ici, je fais les choses différemment. Je veux regarder si le nombre de "visiteur" (
COUNT(DISTINCT user_pseudo_id)
) à diminuer (ou exploser). Je ne regarde donc pas pour des event_name spécifiques, c'est pourquoi j'ai retiré la condition dans leWHERE
.
Afin d'effectuer une prédiction précise, nous avons besoin d'un certain volume d'historique. Si l'historique est trop grand, ça va nuire au modèle (les données AVANT la pandémie n'ont probablement plus rien à voir avec celles d'aujourd'hui.) Mais prendre une période trop petite ne nous permettera pas de détecter des tendances lourdes ou certaines saisonalités. C'est pourquoi je prends ici les 120 derniers jours.
Google sépare les données exportées de GA4 dans une table par jour. Donc, si nous voulons effectuer une requête sur plus d'une journée, nous devons utiliser un widlcard dans le nom de la table. C'est pourquoi au lieu de préciser la table exacte dans la requête (event_20220125
), nous utilisons event_*
. Nous limitons ensuite via une condition WHERE
les jours que nous voulons utiliser via _TABLE_SUFFIX
.
Voilà, notre matière première est prête pour être utilisée par BigQuery ML. Le pire est fait, je vous le jure.
Nous allons utiliser les données des derniers jours afin d'effectuer une prédiction pour nos valeurs. C'est pourquoi nous n'allons pas utiliser les 7 derniers jours pour entraîner notre modèle. Ensuite, en comparant les valeurs réelles des 7 derniers jours avec la prédiction de ces mêmes jours, nous serons en mesure d'identifier des écarts entre la réalité et la prédiction. Ces écarts devront ensuite être analysés afin de déterminer si ce sont de faux positifs ou de vraies anomalies que nous devons corriger.
BigQuery ML est très puissant et permet de créer des modèles complexes avec une facilité déconcertante. Nous avons simplement besoin d'une requête SQL :
CREATE OR REPLACE MODEL `PROJET.DATASET.detection_anomalies_model`
OPTIONS(MODEL_TYPE='ARIMA_PLUS',
AUTO_ARIMA = TRUE, # Nécessaire pour une prédiction multiples.
DATA_FREQUENCY = 'AUTO_FREQUENCY',
HOLIDAY_REGION = "CA", # Inclure les jours fériés canadiens dans le modèle.
time_series_timestamp_col='ts',
time_series_data_col='value',
time_series_id_col='dimension',
horizon = 7) AS
SELECT
*
FROM
`PROJET.DATASET.detection_anomalies_data`
WHERE
ts < TIMESTAMP_SUB(TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 DAY), INTERVAL 7 DAY) # Important, exclure les 7 derniers jours pour entraîner nos modèles.
C'est tout. Oui, oui je vous jure. Le modèle est entraîner. En fait LES modèles devrais-je dire, car BigQuery ML a entraîné autant de modèles que nous avions de dimensions distinguent dans la colonne dimension.
- MODEL_TYPE = ARIMA_PLUS - Indiquer que nous désirons créer un modèle ARIMA+. Ce type de modèles est fait pour l'analyse des séries chronologiques. Avec BigQueryML il est possible de créer plusieurs autres types de modèles.
- AUTO_ARIMA = TRUE - Mettre à TRUE car nous avons entraîner PLUSIEURS modèles.
- DATA_FREQUENCY = AUTO_FREQUENCEY - Détecter la fréquence de notre série chronologique. (Est-ce des valeurs par heures/jour/mois?)
- HOLIDAY_REGION = CA - Indiquer de prendre en compte les jours fériés Canadien dans le modèle.
- time_series_timestamp_col = 'ts' - Indiquer quel champ contient le timestamp.
- time_series_data_col = 'dimension' - Indiquer quel champ contient la valeur que nous souhaitons prédire.
- time_series_id_col = 'value' - Indiquer que le champ dimension contient le nom de la dimension que nous voulons prédire.
- horizon = 7 - Indiquer que nous voulons faire une prédiction sur un horizon de 7 jours.
Pour en savoir plus sur le modèle ARIMA+
Maintenant que nous avons entraîné nos modèles, nous désirons effectuer une prédiction pour les 7 prochains jours avec un intervalle de confiance de 95%. Encore une fois, c'est aussi simple que d'exécuter cette requête :
SELECT
forecast_timestamp, dimension, forecast_value, prediction_interval_lower_bound, prediction_interval_upper_bound
FROM
ML.FORECAST(MODEL `PROJET.DATASET.detection_anomalies_model`, STRUCT(7 AS horizon, 0.95 AS confidence_level))
Voici de quoi ressemble une prédiction :
Il ne reste ensuite qu'à comparer ces prédictions avec la réalité. Un vérifiant si la valeur réelle est plus grande que l'intervalle le plus élevé prédit (ou au contraire, si la valeur est plus petite que l'intervalle le plus petit prédit), il est possible d'identifier des anomalies. Il est même possible de le faire dans une seule requête en effectuant une jointure entre la table de prédiction et la table des valeurs réelles :
WITH
prediction AS (
SELECT
forecast_timestamp, dimension, forecast_value, prediction_interval_lower_bound, prediction_interval_upper_bound
FROM
ML.FORECAST(MODEL `PROJET.DATASET.detection_anomalies_model`, STRUCT(7 AS horizon, 0.95 AS confidence_level))
),
reel as (
SELECT
*
FROM
`PROJET.DATASET.detection_anomalies_data`
WHERE
ts >= TIMESTAMP_SUB(TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 DAY), INTERVAL 7 DAY) # Inclure seulement les 7 derniers jours pour évaluer nos modèles.
)
SELECT
prediction.*,
reel.value,
IF (reel.value > prediction_interval_upper_bound OR reel.value < prediction_interval_lower_bound, "Anomalie", "Normal") AS anomalie
FROM prediction INNER JOIN reel
ON
prediction.forecast_timestamp = reel.ts
AND prediction.dimension = reel.dimension
Finalement, il est même possible d'utiliser Lokker Studio afin d'inspecter les données.
Évidemment, ici les données sont bidons, mais ça vous montre quand même l'idée.
Voilà,, avec 3 requêtes SQL, nous avons : préparer nos données, entraîner un modèle et effectuer des prédictions avec modèle pour identifier les anomalies possibles.
Il est possible d'automatiser l'exécution d'une requête depuis la console web BigQuery. Il est donc possible d'automatiser nos 3 requêtes, mais attention de le faire dans le bon ordre. Par exemple, chaque dimanche :
- Préparation des données à 18h00.
- Entraînement des modèles à 18h10.
- Comparer les prédictions et les valeurs réelles à 18h20.
Ce n'est pas plus compliqué que ça.