Quand deux équipes lisent des chiffres différents pour le même KPI, la confiance se brise. Un metrics layer sert de source de vérité partagée: définitions claires, calculs centralisés, exposition uniforme vers les outils (BI, notebooks, apps). L’objectif est simple: une définition, plusieurs usages, zéro surprise.
prérequis
- Glossaire de KPIs avec owner, formule, périmètre.
- Entrepôt de données fiable (schémas, historisation).
- Un mécanisme d’exposition commun (vues SQL, API, ou semantic layer).
aperçu rapide
- Écrire la définition du KPI en texte et en code, au même endroit.
- Centraliser le calcul en vue/materialized view, pas dans les dashboards.
- Versionner les définitions, annoncer les changements.
- Ajouter des tests de qualité et des garde-fous d’ordres de grandeur.
- Exposer via un contrat stable: colonnes, types, granularité.
- Journaliser l’usage et mesurer l’impact sur les décisions.
tutoriel pas-à-pas
étape 1: formaliser la définition du KPI
Texte court, formule, périmètre, owner, et lien vers le calcul.
kpi: taux_de_reachat_30j
definition: "clients qui rachètent dans les 30 jours / clients uniques"
perimetre: "FR, web"
owner: "ops"
source: "warehouse.analytics.orders"
changelog:
- date: "2020-06-20"
change: "exclure commandes test"
étape 2: centraliser le calcul en vue stable
La logique vit dans l’entrepôt, pas dans chaque outil.
-- vue centrale: taux de réachat à 30 jours par période
CREATE OR REPLACE VIEW metrics.taux_reachat_30j_v AS
WITH first_orders AS (
SELECT user_id, MIN(order_date)::date AS first_date
FROM analytics.orders
WHERE is_test = false AND country = 'FR' AND channel = 'web'
GROUP BY user_id
),
reorders AS (
SELECT o.user_id, o.order_date::date AS order_date
FROM analytics.orders o
JOIN first_orders f ON o.user_id = f.user_id
WHERE o.order_date::date <= f.first_date + INTERVAL '30 day'
AND o.order_date::date > f.first_date
AND o.is_test = false
)
SELECT
DATE_TRUNC('week', f.first_date)::date AS semaine_cohorte,
COUNT(DISTINCT f.user_id) AS clients_cohorte,
COUNT(DISTINCT r.user_id) AS clients_reacheteurs,
ROUND(100.0 * COUNT(DISTINCT r.user_id) / NULLIF(COUNT(DISTINCT f.user_id),0), 1) AS taux_reachat_30j_pct
FROM first_orders f
LEFT JOIN reorders r ON r.user_id = f.user_id
GROUP BY 1
ORDER BY 1 DESC;
étape 3: publier un contrat de schéma
Stabiliser noms, types, granularité, et champs obligatoires.
metric_contract:
name: metrics.taux_reachat_30j_v
granularity: "week"
columns:
- name: semaine_cohorte
type: date
required: true
- name: clients_cohorte
type: integer
required: true
- name: clients_reacheteurs
type: integer
required: true
- name: taux_reachat_30j_pct
type: numeric(5,1)
required: true
slos:
freshness_max_minutes: 180
row_count_min: 4
étape 4: ajouter des tests de qualité
Bloquer les régressions avant qu’elles n’atteignent les outils.
-- test 1: bornes plausibles
SELECT 1
FROM metrics.taux_reachat_30j_v
WHERE taux_reachat_30j_pct < 0 OR taux_reachat_30j_pct > 100;
-- test 2: fraîcheur
SELECT CASE WHEN MAX(semaine_cohorte) >= DATE_TRUNC('week', CURRENT_DATE) - INTERVAL '28 day'
THEN 'OK' ELSE 'STALE' END AS freshness;
étape 5: exposer partout la même chose
Consommer la vue depuis BI, notebooks, API, sans re-coder la logique.
-- exemple usage BI
SELECT semaine_cohorte, taux_reachat_30j_pct
FROM metrics.taux_reachat_30j_v
ORDER BY semaine_cohorte DESC
LIMIT 12;
# exemple usage notebook
import pandas as pd
import sqlalchemy as sa
engine = sa.create_engine("postgresql://...")
df = pd.read_sql("SELECT * FROM metrics.taux_reachat_30j_v", engine)
print(df.head())
étape 6: gérer versions et changements
Protéger les analyses en cours, annoncer les ruptures.
changement majeur planifié:
- date: 2020-12-20
- impact: inclure commandes marketplace
- plan: publier v2 en parallèle 2 semaines, retrait v1 le 2021-01-03
exemples
cas: regrouper plusieurs métriques clés dans un même layer
CREATE OR REPLACE VIEW metrics.marketing_kpis_v AS
SELECT
d::date AS jour,
SUM(spend_eur) AS spend_eur,
SUM(clicks) AS clicks,
ROUND(100.0 * SUM(conversions)/NULLIF(SUM(clicks),0),2) AS ctr_pct,
ROUND(SUM(spend_eur)/NULLIF(SUM(conversions),0),2) AS cpa_eur
FROM marketing.daily_stats
WHERE d >= CURRENT_DATE - INTERVAL '90 day'
GROUP BY 1;
documentation:
spend_eur: "dépenses publicitaires en EUR"
ctr_pct: "clicks / impressions, en %"
cpa_eur: "spend / conversions, en EUR, hors taxes"
erreurs courantes et solutions
- calculs dupliqués dans chaque dashboard -> divergences -> centraliser en vues et interdire la logique métier dans l’outil
- définitions mouvantes -> débats -> versionner, dater, publier changelog
- champs cachés ou renommés sans préavis -> casse -> contrat de schéma et dépréciation progressive
- tests absents -> surprises -> tests de bornes, de fraîcheur et d’ordres de grandeur
- granularités mélangées -> incohérences -> préciser la granularité et fournir des vues par niveau (jour, semaine, mois)
- accès direct aux tables brutes -> dette -> exposer uniquement des vues stables côté consommateurs
faq
- faut-il un outil de semantic layer dédié ? Pas obligatoire. Des vues SQL bien conçues sont un bon début. Un outil dédié aide quand les besoins grandissent.
- comment gérer les métriques “produit” vs “marketing” ? Un domaine par métrique, des owners clairs, et des revues croisées pour les métriques partagées.
- que faire si une équipe a besoin d’une variante ? Proposer un suffixe explicite (ex: _alt) et documenter la différence. Si la variante gagne, elle remplace l’original après une période de chevauchement.