Analytics et préjugés : pourquoi les métriques n'intéressent pas que mon chat

Temps de lecture : 9 min

Découvrez pourquoi chaque clic vaut son pesant de clic, même si votre chat en doute.

"Les métriques, c'est comme les opinions : tout le monde en a, mais personne ne veut entendre celles des autres."

Pourquoi on s'emmerde avec des chiffres ?

Alors tu vois, on pourrait se contenter d'écrire des articles de qualité et attendre que les Internets nous envoient des chocolats de remerciement. Mais NON. On est des psychopathes du data-driven, alors on a mis en place un système complet pour savoir EXACTEMENT combien de personnes ont eu le malheur de cliquer sur nos liens.

Est-ce qu'on avait besoin de ça ? Non. Est-ce qu'on l'a fait quand même ? Oui. Parce que c'est ça, être un développeur en 2023 2024 2025 (raye la mention inutile, ou toutes, comme ton âme).

Architecture de la chose

Notre système de métriques est composé de trois grandes parties :

  1. Récupération des données depuis Umami (parce que Google Analytics c'est has-been et ça mange des cookies)
  2. Traitement et pondération des métriques (parce que tous les clics ne se valent pas, comme les mecs)
  3. Exploitation pour les Top Articles (parce qu'il faut bien mettre des paillettes dans la vie des gens)
   Umami       →    Notre Système    →    Résultats Pondérés
 (données brutes)    (algorithmes)        (articles populaires)

Composants principaux : la dream teamp des métriques

Les Commandes (a.k.a "les Ordres qu'on donne au serveur")

On a trois commandes essentielles, comme les trois repas par jour, sauf que là c'est vraiment utile :

1. FetchUmamiStatsCommand : Le Ramasseur de Données

#[AsCommand(
    name: 'app:umami:fetch-stats',
    description: 'Fetch Umami stats and dispatch messages to process them',
)]
class FetchUmamiStatsCommand extends Command
{
    // Le code qui va chercher des données comme si c'était des truffes
}

Ce petit bijou de technologie fait une chose simple : il va chercher les stats sur Umami et les balance dans notre système de messages. C'est comme envoyer un stagiaire chercher du café, sauf que là ça marche.

2. CleanMetricsDuplicatesCommand : Le Nettoyeur

#[AsCommand(
    name: 'app:metrics:clean-duplicates',
    description: 'Nettoie les entrées en double dans la table page_metrics',
)]
class CleanMetricsDuplicatesCommand extends Command
{
    // Le code qui fait le ménage comme ta mère t'a toujours dit de le faire
}

Cette commande, c'est Marie Kondo mais pour les données. Elle regarde tes métriques en double et se demande : "Est-ce que cette métrique me procure de la joie ?" Si non, elle dégage.

3. RecalculateScoresCommand : le juge de Drag Race

#[AsCommand(
    name: 'app:metrics:recalculate-scores',
    description: 'Recalcule les scores pondérés de tous les articles',
)]
class RecalculateScoresCommand extends Command
{
    // Le code qui note tes articles comme un jury de patinage artistique
}

Cette commande recalcule les scores de tous tes articles, parce que ton article sur "Comment faire des poneys avec Symfony" mérite peut-être mieux que sa 47ème place actuelle. Ou pas.

Le Service de Pondération : le génie du système

Le cœur de notre système de génialitude, c'est AdvancedScoreCalculator. Si Einstein avait codé des systèmes de recommandation d'articles, ça aurait ressemblé à ça (en moins bien).

class AdvancedScoreCalculator
{
    // Facteurs influençant le score final
    private const float POPULARITY_WEIGHT = 0.4;   // Popularité (vues)
    private const float RECENCY_WEIGHT = 0.25;     // Récence du contenu
    private const float TREND_WEIGHT = 0.2;        // Tendance récente (augmentation des vues)
    private const float DIVERSITY_WEIGHT = 0.15;   // Diversité (fréquence d'apparition)

    // Configuration
    private const int MAX_RECENT_DAYS = 90;        // Récence maximale (3 mois)
    private const int TREND_DAYS = 7;              // Période pour la tendance (7 jours)
    
    // Et plein de méthodes compliquées qui font des maths
}

Cet algorithme est tellement sophistiqué qu'il prend en compte :

Workflow complet : comment ça marche bordel ?

  1. Umami collecte les vues en temps réel sur ton site (les gens qui cliquent, scrollent, et parfois même lisent)

  2. Toutes les 3 heures, la commande app:uami:fetch-stats se déclenche et va chercher les données :

    # Ça lance ce truc
    symfony console app:umami:fetch-stats
    
  3. Pour chaque URL visitée, un message ProcessPageMetricsMessage est envoyé :

    $message = new ProcessPageMetricsMessage(
        $metric->getUrl(),
        $metric->getViews(),
    );
    
  4. Le handler de messages reçoit et traite ces informations en créant des PageMetric :

    $metric = new PageMetric();
    $metric->setUrl($message->getUrl());
    $metric->setViews($message->getViews());
    
  5. Toutes les 6 heures, les scores sont recalculés avec notre algorithme avancé :

    # Ça lance l'algorithme qui fait des maths comme si ta vie en dépendait
    symfony console app:metrics:recalculate-scores
    
  6. Une fois par semaine (le dimanche à 4h30 du matin, quand même les fêtards sont couchés), on nettoie les données en double :

    # Nettoyage comme si Marie Kondo passait sur tes données
    symfony console app:metrics:clean-duplicates
    
  7. Le composant TopPosts utilise ces scores pour afficher les articles les plus intéressants :

    // Quelque part dans le code qui génère les recommandations
    $diverseTopArticles = $this->scoreCalculator->getDiverseTopArticles(5);
    

CRONs : la planification des basses œuvres

Dans .platform.app.yaml, on a configuré des tâches planifiées qui s'exécutent automatiquement :

crons:
    # Métriques - données Umami et pondération
    umami-fetch:
        # Récupère les statistiques de trafic depuis Umami toutes les 3 heures à :15 (ex: 3h15, 6h15, 9h15...)
        # Cette commande collecte les données de pages vues pour les articles
        spec: '15 */3 * * *'
        cmd: if [ "$PLATFORM_ENVIRONMENT_TYPE" = "production" ]; then croncape symfony console app:metrics:fetch-stats; fi
    recalculate-scores:
        # Recalcule les scores pondérés pour le classement d'articles toutes les 6 heures à :45 (ex: 5h45, 11h45, 17h45...)
        # Utilise l'algorithme AdvancedScoreCalculator pour calculer la popularité, la récence, la tendance et la diversité
        spec: '45 */6 * * *'
        cmd: if [ "$PLATFORM_ENVIRONMENT_TYPE" = "production" ]; then croncape symfony console app:metrics:recalculate-scores; fi
    clean-metrics:
        # Nettoie les doublons de métriques une fois par semaine (dimanche à 4h30 du matin)
        # Cette opération de maintenance garantit l'intégrité de la base de données des métriques
        spec: '30 4 * * 0'
        cmd: if [ "$PLATFORM_ENVIRONMENT_TYPE" = "production" ]; then croncape symfony console app:metrics:clean-duplicates; fi

Ça s'exécute automatiquement, comme ton anxiété quand tu reçois un message "On peut parler ?" de ton chef.

Algorithme de pondération : la sauce secrète

L'algorithme qui calcule les scores est plus complexe que ton dernier rendez-vous Tinder. Il prend en compte quatre facteurs principaux :

1. Score de Popularité (40% du score final)

private function calculatePopularityScore(int $views): float
{
    // log10(vues + 1) pour éviter log(0) et normaliser les résultats
    // Un article avec 1000 vues aura un score de 0.75, 10000 = 1.0
    return min(1.0, log10($views + 1) / 4);
}

On utilise une échelle logarithmique parce que 10 000 vues, c'est pas 10 fois mieux que 1 000 vues. C'est juste que l'auteur a probablement partagé l'article dans plus de groupes Facebook.

2. Score de Récence (25% du score final)

private function calculateRecencyScore(Post $post): float
{
    $publishedAt = $post->getPublishedAt();
    if (!$publishedAt instanceof \DateTimeImmutable) {
        return 0;
    }

    $now = new DateTime();
    $daysSincePublished = $now->diff($publishedAt)->days;

    // Score dégressif en fonction de l'ancienneté
    if ($daysSincePublished > self::MAX_RECENT_DAYS) {
        return 0.1; // Score minimum pour les articles très anciens
    }

    return 1 - (0.9 * $daysSincePublished / self::MAX_RECENT_DAYS);
}

Parce que ton article sur "Comment installer Windows Vista" n'est peut-être plus aussi pertinent en 2024.

3. Score de Tendance (20% du score final)

private function calculateTrendScore(Post $post): float
{
    // Si le post a moins de TREND_DAYS jours, lui donner un bonus de tendance
    $publishedAt = $post->getPublishedAt();
    if (!$publishedAt instanceof \DateTimeImmutable) {
        return 0.5; // Valeur moyenne par défaut
    }

    $now = new DateTime();
    $age = $now->diff($publishedAt)->days;

    // Si l'article est très récent, lui donner un bonus
    if ($age <= self::TREND_DAYS) {
        return 0.8 + (0.2 * (self::TREND_DAYS - $age) / self::TREND_DAYS);
    }

    return 0.5;
}

Ceci favorise les articles qui sont en train de monter en popularité, comme un nouveau mème sur Twitter avant qu'Elon ne le ruine.

4. Score de Diversité (15% du score final)

private function calculateDiversityScore(Post $post): float
{
    // Logique complexe pour éviter de toujours montrer les mêmes articles
    // ...

    // Si l'article a beaucoup de vues, réduire son score de diversité
    if ($views > 10000) {
        return 0.4; // Forte pénalité pour les articles très populaires
    }

    // ...
}

Ceci évite l'effet "DJ qui passe toujours les mêmes chansons". On veut parfois découvrir autre chose que "Despacito".

Sélection finale : comment on choisit les gagnants

La méthode qui sélectionne les articles à afficher est un chef-d'œuvre d'équilibrage entre popularité et diversité :

public function getDiverseTopArticles(int $limit = 5): array
{
    // 1. Récupérer plus d'articles que nécessaire (pour avoir de la marge)
    $candidates = $this->metricRepository->findTopArticlesWithWeighting($limit * 2);

    // 2. Toujours prendre les 2 premiers (les meilleurs scores)
    // ...

    // 3. Pour les places restantes, faire une sélection pseudo-aléatoire pondérée
    // avec un facteur aléatoire qui réduit légèrement l'importance du score
    $randomFactor = (random_int(0, 100) / 100) * 0.3; // 30% d'aléatoire
    // ...
}

On garde les deux articles vraiment populaires, puis on ajoute un peu de hasard pour les autres places. Comme la playlist Spotify en mode shuffle qui joue quand même toujours du Queen.

Le fin mot de tout ça

Tu viens de découvrir un système de métriques plus sophistiqué que la politique monétaire de la BCE, mais moins mystérieux que l'algorithme de recommandation de TikTok.

En résumé :

C'est comme ça qu'on fait des recommandations d'articles en 2024, et c'est exactement comme ça que Google fonctionne (à quelques milliards de dollars de budget R&D près).

Liens utiles

"Les métriques, c'est comme les abdos : tout le monde en veut, mais personne ne veut faire l'effort de les obtenir."

Confidentialité

Ce site utilise Umami pour analyser le trafic de manière anonyme. Acceptez-vous la collecte de données anonymes ?