← Retour au blog

Metrics layer: source de vérité partagée

Lucian BLETAN

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.