Exercices Pratiques - Agrégation MongoDB

Maîtrisez le framework d'agrégation à travers des cas pratiques

1 Premier Pipeline : Statistiques de Base Facile

📋 Contexte

Vous travaillez pour "MarocMart", une chaîne de supermarchés marocaine. Analysons les ventes de la collection sales.

Structure des documents

{
    "_id": ObjectId("..."),
    "date": ISODate("2024-03-15T10:30:00Z"),
    "store": {
        "id": "STR001",
        "name": "MarocMart Maarif",
        "city": "Casablanca"
    },
    "items": [
        {
            "product": "Lait demi-écrémé",
            "category": "Produits laitiers",
            "price": 7.50,
            "quantity": 2
        },
        {
            "product": "Pain complet",
            "category": "Boulangerie",
            "price": 3.50,
            "quantity": 1
        }
    ],
    "total": 18.50,
    "paymentMethod": "cash"
}

🎯 Tâches

  1. Calculez le total des ventes pour Mars 2024
  2. Trouvez la vente moyenne
  3. Comptez le nombre de transactions
  4. Identifiez les méthodes de paiement utilisées
💡 Indice : Utilisez $match pour filtrer les dates, puis $group avec les opérateurs d'accumulation appropriés.

📖 Solution complète :

db.sales.aggregate([
    // Stage 1 : Filtrer Mars 2024
    { $match: {
        date: {
            $gte: ISODate("2024-03-01T00:00:00Z"),
            $lt: ISODate("2024-04-01T00:00:00Z")
        }
    }},
    
    // Stage 2 : Calculer les statistiques
    { $group: {
        _id: null,  // Regrouper tous les documents
        
        totalRevenue: { $sum: "$total" },
        avgSale: { $avg: "$total" },
        minSale: { $min: "$total" },
        maxSale: { $max: "$total" },
        transactionCount: { $sum: 1 },
        
        // Collecter les méthodes de paiement uniques
        paymentMethods: { $addToSet: "$paymentMethod" }
    }},
    
    // Stage 3 : Formater le résultat
    { $project: {
        _id: 0,
        "Statistiques Mars 2024": {
            "Chiffre d'affaires total": { $round: ["$totalRevenue", 2] },
            "Vente moyenne": { $round: ["$avgSale", 2] },
            "Vente minimale": "$minSale",
            "Vente maximale": "$maxSale",
            "Nombre de transactions": "$transactionCount",
            "Méthodes de paiement": "$paymentMethods"
        }
    }}
])

// Résultat attendu :
{
    "Statistiques Mars 2024": {
        "Chiffre d'affaires total": 458750.50,
        "Vente moyenne": 76.46,
        "Vente minimale": 3.50,
        "Vente maximale": 890.00,
        "Nombre de transactions": 6000,
        "Méthodes de paiement": ["cash", "card", "mobile"]
    }
}
✅ Points clés :
  • $match en premier pour filtrer efficacement
  • $group avec _id: null pour agréger tous les documents
  • $addToSet pour les valeurs uniques
  • $round pour formater les nombres décimaux

2 Analyse par Magasin et Catégorie Moyen

🎯 Tâches

Créez un rapport montrant pour chaque magasin :

  1. Le chiffre d'affaires total par catégorie de produit
  2. Les 3 produits les plus vendus
  3. La répartition des méthodes de paiement
💡 Indice : Vous devrez utiliser $unwind pour décomposer le tableau items, puis plusieurs $group successifs.

📖 Solution complète :

db.sales.aggregate([
    // Stage 1 : Décomposer les articles
    { $unwind: "$items" },
    
    // Stage 2 : Calculer le montant par article
    { $addFields: {
        "items.totalAmount": {
            $multiply: ["$items.price", "$items.quantity"]
        }
    }},
    
    // Stage 3 : Regrouper par magasin et catégorie
    { $group: {
        _id: {
            storeId: "$store.id",
            storeName: "$store.name",
            storeCity: "$store.city",
            category: "$items.category"
        },
        categoryRevenue: { $sum: "$items.totalAmount" },
        products: {
            $push: {
                name: "$items.product",
                quantity: "$items.quantity"
            }
        },
        transactions: { $addToSet: "$_id" }
    }},
    
    // Stage 4 : Calculer les top produits par catégorie
    { $addFields: {
        topProducts: {
            $slice: [
                { $sortArray: {
                    input: "$products",
                    sortBy: { quantity: -1 }
                }},
                3
            ]
        }
    }},
    
    // Stage 5 : Regrouper par magasin
    { $group: {
        _id: {
            storeId: "$_id.storeId",
            storeName: "$_id.storeName",
            storeCity: "$_id.storeCity"
        },
        categoriesBreakdown: {
            $push: {
                category: "$_id.category",
                revenue: { $round: ["$categoryRevenue", 2] }
            }
        },
        totalRevenue: { $sum: "$categoryRevenue" },
        transactionCount: { 
            $sum: { $size: "$transactions" }
        }
    }},
    
    // Stage 6 : Jointure pour les méthodes de paiement
    { $lookup: {
        from: "sales",
        let: { storeId: "$_id.storeId" },
        pipeline: [
            { $match: {
                $expr: { $eq: ["$store.id", "$$storeId"] }
            }},
            { $group: {
                _id: "$paymentMethod",
                count: { $sum: 1 }
            }}
        ],
        as: "paymentMethods"
    }},
    
    // Stage 7 : Formater le résultat final
    { $project: {
        _id: 0,
        store: {
            id: "$_id.storeId",
            name: "$_id.storeName",
            city: "$_id.storeCity"
        },
        totalRevenue: { $round: ["$totalRevenue", 2] },
        transactionCount: 1,
        categoriesBreakdown: {
            $sortArray: {
                input: "$categoriesBreakdown",
                sortBy: { revenue: -1 }
            }
        },
        paymentMethods: {
            $map: {
                input: "$paymentMethods",
                as: "pm",
                in: {
                    method: "$$pm._id",
                    percentage: {
                        $round: [
                            { $multiply: [
                                { $divide: ["$$pm.count", "$transactionCount"] },
                                100
                            ]},
                            1
                        ]
                    }
                }
            }
        }
    }},
    
    { $sort: { totalRevenue: -1 } }
])

// Exemple de résultat pour un magasin :
{
    "store": {
        "id": "STR001",
        "name": "MarocMart Maarif",
        "city": "Casablanca"
    },
    "totalRevenue": 125430.75,
    "transactionCount": 1543,
    "categoriesBreakdown": [
        { "category": "Produits laitiers", "revenue": 34521.50 },
        { "category": "Fruits et légumes", "revenue": 28900.25 },
        { "category": "Boulangerie", "revenue": 22100.00 }
    ],
    "paymentMethods": [
        { "method": "card", "percentage": 45.5 },
        { "method": "cash", "percentage": 42.3 },
        { "method": "mobile", "percentage": 12.2 }
    ]
}
$unwind
$group
$group
$lookup
$project

3 Analyse Temporelle avec $facet Moyen

📋 Contexte

La direction de MarocMart veut un dashboard complet pour analyser les patterns de vente.

🎯 Tâches

Créez un pipeline unique qui fournit :

  1. Les ventes par jour de la semaine
  2. Les ventes par heure de la journée
  3. L'évolution mensuelle sur l'année
  4. Le top 5 des jours avec le plus de ventes
💡 Indice : Utilisez $facet pour exécuter plusieurs analyses en parallèle. Les opérateurs de date comme $dayOfWeek et $hour seront essentiels.

📖 Solution complète :

db.sales.aggregate([
    // Filtrer l'année 2024
    { $match: {
        date: {
            $gte: ISODate("2024-01-01"),
            $lt: ISODate("2025-01-01")
        }
    }},
    
    // Analyses multiples avec $facet
    { $facet: {
        // Analyse 1 : Par jour de la semaine
        "ventesParJourSemaine": [
            { $group: {
                _id: { $dayOfWeek: "$date" },
                totalVentes: { $sum: "$total" },
                nombreTransactions: { $sum: 1 },
                ticketMoyen: { $avg: "$total" }
            }},
            { $project: {
                jour: {
                    $switch: {
                        branches: [
                            { case: { $eq: ["$_id", 1] }, then: "Dimanche" },
                            { case: { $eq: ["$_id", 2] }, then: "Lundi" },
                            { case: { $eq: ["$_id", 3] }, then: "Mardi" },
                            { case: { $eq: ["$_id", 4] }, then: "Mercredi" },
                            { case: { $eq: ["$_id", 5] }, then: "Jeudi" },
                            { case: { $eq: ["$_id", 6] }, then: "Vendredi" },
                            { case: { $eq: ["$_id", 7] }, then: "Samedi" }
                        ]
                    }
                },
                totalVentes: { $round: ["$totalVentes", 2] },
                nombreTransactions: 1,
                ticketMoyen: { $round: ["$ticketMoyen", 2] }
            }},
            { $sort: { "_id": 1 } }
        ],
        
        // Analyse 2 : Par heure de la journée
        "ventesParHeure": [
            { $group: {
                _id: { $hour: "$date" },
                totalVentes: { $sum: "$total" },
                nombreTransactions: { $sum: 1 }
            }},
            { $project: {
                heure: { 
                    $concat: [
                        { $toString: "$_id" },
                        "h00"
                    ]
                },
                totalVentes: { $round: ["$totalVentes", 2] },
                nombreTransactions: 1,
                // Indicateur de période de pointe
                periodePointe: {
                    $cond: {
                        if: { 
                            $or: [
                                { $and: [{ $gte: ["$_id", 11] }, { $lte: ["$_id", 14] }] },
                                { $and: [{ $gte: ["$_id", 18] }, { $lte: ["$_id", 20] }] }
                            ]
                        },
                        then: true,
                        else: false
                    }
                }
            }},
            { $sort: { "_id": 1 } }
        ],
        
        // Analyse 3 : Évolution mensuelle
        "evolutionMensuelle": [
            { $group: {
                _id: {
                    year: { $year: "$date" },
                    month: { $month: "$date" }
                },
                totalVentes: { $sum: "$total" },
                nombreTransactions: { $sum: 1 },
                ticketMoyen: { $avg: "$total" }
            }},
            { $project: {
                mois: {
                    $dateToString: {
                        format: "%Y-%m",
                        date: {
                            $dateFromParts: {
                                year: "$_id.year",
                                month: "$_id.month"
                            }
                        }
                    }
                },
                totalVentes: { $round: ["$totalVentes", 2] },
                nombreTransactions: 1,
                ticketMoyen: { $round: ["$ticketMoyen", 2] },
                // Calcul de la croissance (nécessite une jointure ou window function)
                croissance: null
            }},
            { $sort: { mois: 1 } }
        ],
        
        // Analyse 4 : Top 5 jours
        "top5Jours": [
            { $group: {
                _id: {
                    $dateToString: {
                        format: "%Y-%m-%d",
                        date: "$date"
                    }
                },
                totalVentes: { $sum: "$total" },
                nombreTransactions: { $sum: 1 }
            }},
            { $sort: { totalVentes: -1 } },
            { $limit: 5 },
            { $project: {
                date: "$_id",
                totalVentes: { $round: ["$totalVentes", 2] },
                nombreTransactions: 1,
                // Ajouter le jour de la semaine pour contexte
                jourSemaine: {
                    $dayOfWeek: {
                        $dateFromString: {
                            dateString: "$_id"
                        }
                    }
                }
            }}
        ],
        
        // Bonus : Statistiques globales
        "statistiquesGlobales": [
            { $group: {
                _id: null,
                totalAnnuel: { $sum: "$total" },
                moyenneJournaliere: { $avg: "$total" },
                totalTransactions: { $sum: 1 }
            }},
            { $project: {
                _id: 0,
                totalAnnuel: { $round: ["$totalAnnuel", 2] },
                moyenneJournaliere: { $round: ["$moyenneJournaliere", 2] },
                totalTransactions: 1,
                previsionAnnuelle: {
                    $round: [
                        { $multiply: [
                            "$moyenneJournaliere",
                            365
                        ]},
                        2
                    ]
                }
            }}
        ]
    }}
])
Structure du résultat :
{
    "ventesParJourSemaine": [
        { "jour": "Lundi", "totalVentes": 125000, "nombreTransactions": 1650, "ticketMoyen": 75.76 },
        { "jour": "Samedi", "totalVentes": 198000, "nombreTransactions": 2450, "ticketMoyen": 80.82 }
        // ...
    ],
    "ventesParHeure": [
        { "heure": "9h00", "totalVentes": 45000, "nombreTransactions": 580, "periodePointe": false },
        { "heure": "12h00", "totalVentes": 89000, "nombreTransactions": 1120, "periodePointe": true }
        // ...
    ],
    "evolutionMensuelle": [
        { "mois": "2024-01", "totalVentes": 850000, "nombreTransactions": 11200, "ticketMoyen": 75.89 }
        // ...
    ],
    "top5Jours": [
        { "date": "2024-12-24", "totalVentes": 45600, "nombreTransactions": 890 }
        // ...
    ],
    "statistiquesGlobales": [{
        "totalAnnuel": 12500000,
        "moyenneJournaliere": 34246.58,
        "totalTransactions": 165000
    }]
}
💡 Avantages de $facet :
  • Une seule lecture de la collection
  • Analyses multiples en parallèle
  • Résultat structuré pour dashboards
  • Performance optimale

4 Jointures Complexes avec $lookup Moyen

📋 Contexte

MarocMart a trois collections liées :

  • sales : Les ventes
  • customers : Les clients (programme fidélité)
  • products : Le catalogue produits

Collection customers

{
    "_id": "CUST001",
    "name": "Fatima Benali",
    "email": "f.benali@email.ma",
    "loyaltyCard": "GOLD",
    "points": 2450,
    "registeredDate": ISODate("2022-03-15")
}

Collection products

{
    "_id": "PROD001",
    "name": "Huile d'olive extra vierge",
    "category": "Épicerie",
    "supplier": "Coopérative Atlas",
    "price": 45.00,
    "stock": 150
}

🎯 Tâches

  1. Pour chaque vente, enrichissez avec les infos client et produit
  2. Calculez les points de fidélité gagnés (1 point par 10 DH)
  3. Identifiez les produits en rupture de stock vendus
💡 Indice : Utilisez $lookup avec pipeline pour des jointures conditionnelles. N'oubliez pas $unwind après les lookups qui retournent des tableaux.

📖 Solution complète :

db.sales.aggregate([
    // Limiter pour l'exemple
    { $limit: 100 },
    
    // Jointure avec customers
    { $lookup: {
        from: "customers",
        localField: "customerId",
        foreignField: "_id",
        as: "customer"
    }},
    
    // Dérouler le customer (0 ou 1)
    { $unwind: {
        path: "$customer",
        preserveNullAndEmptyArrays: true  // Garder les ventes sans client fidélité
    }},
    
    // Décomposer les items pour enrichir chaque produit
    { $unwind: "$items" },
    
    // Jointure avancée avec products
    { $lookup: {
        from: "products",
        let: { 
            productName: "$items.product",
            quantity: "$items.quantity"
        },
        pipeline: [
            // Matcher sur le nom du produit
            { $match: {
                $expr: { $eq: ["$name", "$$productName"] }
            }},
            // Vérifier le stock
            { $project: {
                _id: 1,
                name: 1,
                category: 1,
                supplier: 1,
                currentStock: "$stock",
                stockStatus: {
                    $cond: {
                        if: { $lte: ["$stock", "$$quantity"] },
                        then: "Rupture proche",
                        else: "Stock OK"
                    }
                }
            }}
        ],
        as: "productInfo"
    }},
    
    { $unwind: {
        path: "$productInfo",
        preserveNullAndEmptyArrays: true
    }},
    
    // Calculer les points de fidélité
    { $addFields: {
        "items.loyaltyPoints": {
            $cond: {
                if: { $ne: ["$customer", null] },
                then: {
                    $floor: {
                        $divide: [
                            { $multiply: ["$items.price", "$items.quantity"] },
                            10
                        ]
                    }
                },
                else: 0
            }
        },
        "items.productDetails": "$productInfo"
    }},
    
    // Regrouper les items
    { $group: {
        _id: "$_id",
        date: { $first: "$date" },
        store: { $first: "$store" },
        customer: { $first: "$customer" },
        total: { $first: "$total" },
        paymentMethod: { $first: "$paymentMethod" },
        items: { $push: "$items" },
        totalLoyaltyPoints: { $sum: "$items.loyaltyPoints" },
        
        // Collecter les produits en rupture
        lowStockItems: {
            $push: {
                $cond: {
                    if: { 
                        $eq: ["$productInfo.stockStatus", "Rupture proche"] 
                    },
                    then: {
                        product: "$items.product",
                        stockRestant: "$productInfo.currentStock"
                    },
                    else: null
                }
            }
        }
    }},
    
    // Nettoyer les valeurs null dans lowStockItems
    { $addFields: {
        lowStockItems: {
            $filter: {
                input: "$lowStockItems",
                as: "item",
                cond: { $ne: ["$$item", null] }
            }
        }
    }},
    
    // Projection finale
    { $project: {
        saleId: "$_id",
        date: 1,
        store: "$store.name",
        
        // Info client enrichie
        customer: {
            $cond: {
                if: { $ne: ["$customer", null] },
                then: {
                    name: "$customer.name",
                    loyaltyCard: "$customer.loyaltyCard",
                    currentPoints: "$customer.points",
                    newPointsEarned: "$totalLoyaltyPoints",
                    totalPointsAfter: { 
                        $add: ["$customer.points", "$totalLoyaltyPoints"] 
                    }
                },
                else: "Client non fidélisé"
            }
        },
        
        // Résumé de la vente
        sale: {
            total: "$total",
            itemCount: { $size: "$items" },
            paymentMethod: "$paymentMethod"
        },
        
        // Alertes stock
        alerts: {
            $cond: {
                if: { $gt: [{ $size: "$lowStockItems" }, 0] },
                then: {
                    hasLowStock: true,
                    items: "$lowStockItems"
                },
                else: {
                    hasLowStock: false
                }
            }
        }
    }}
])

// Exemple de résultat enrichi :
{
    "saleId": ObjectId("..."),
    "date": ISODate("2024-03-15T10:30:00Z"),
    "store": "MarocMart Maarif",
    "customer": {
        "name": "Fatima Benali",
        "loyaltyCard": "GOLD",
        "currentPoints": 2450,
        "newPointsEarned": 18,
        "totalPointsAfter": 2468
    },
    "sale": {
        "total": 185.50,
        "itemCount": 5,
        "paymentMethod": "card"
    },
    "alerts": {
        "hasLowStock": true,
        "items": [
            {
                "product": "Huile d'argan bio",
                "stockRestant": 3
            }
        ]
    }
}
⚠️ Performance : Les jointures multiples peuvent être coûteuses. Assurez-vous d'avoir des index sur les champs de jointure et limitez le nombre de documents traités.

5 Manipulation Avancée de Tableaux Difficile

📋 Contexte

MarocMart veut analyser les paniers clients pour optimiser les promotions et le cross-selling.

🎯 Tâches

  1. Identifiez les produits fréquemment achetés ensemble
  2. Calculez le panier moyen par catégorie
  3. Trouvez les combinaisons de produits les plus rentables
  4. Suggérez des promotions basées sur les associations
💡 Indice : Utilisez $map pour créer des paires de produits, $filter pour éliminer les doublons, et $reduce pour des calculs complexes sur les tableaux.

📖 Solution complète :

// Pipeline pour l'analyse d'association de produits
db.sales.aggregate([
    // Filtrer les paniers avec au moins 2 articles
    { $match: {
        $expr: { $gte: [{ $size: "$items" }, 2] }
    }},
    
    // Créer toutes les paires possibles de produits dans chaque panier
    { $project: {
        store: 1,
        date: 1,
        
        // Générer les paires avec indices
        productPairs: {
            $reduce: {
                input: { $range: [0, { $size: "$items" }] },
                initialValue: [],
                in: {
                    $concatArrays: [
                        "$$value",
                        {
                            $map: {
                                input: { 
                                    $range: [
                                        { $add: ["$$this", 1] }, 
                                        { $size: "$items" }
                                    ]
                                },
                                as: "j",
                                in: {
                                    product1: { $arrayElemAt: ["$items.product", "$$this"] },
                                    product2: { $arrayElemAt: ["$items.product", "$$j"] },
                                    category1: { $arrayElemAt: ["$items.category", "$$this"] },
                                    category2: { $arrayElemAt: ["$items.category", "$$j"] },
                                    revenue: {
                                        $add: [
                                            { $multiply: [
                                                { $arrayElemAt: ["$items.price", "$$this"] },
                                                { $arrayElemAt: ["$items.quantity", "$$this"] }
                                            ]},
                                            { $multiply: [
                                                { $arrayElemAt: ["$items.price", "$$j"] },
                                                { $arrayElemAt: ["$items.quantity", "$$j"] }
                                            ]}
                                        ]
                                    }
                                }
                            }
                        }
                    ]
                }
            }
        },
        
        // Garder les détails du panier pour analyses
        basketDetails: {
            totalItems: { $size: "$items" },
            totalRevenue: "$total",
            categories: { $setUnion: ["$items.category", []] }
        }
    }},
    
    // Décomposer les paires
    { $unwind: "$productPairs" },
    
    // Normaliser l'ordre des paires (A,B) = (B,A)
    { $project: {
        store: 1,
        date: 1,
        basketDetails: 1,
        pair: {
            $cond: {
                if: { $lte: ["$productPairs.product1", "$productPairs.product2"] },
                then: {
                    products: ["$productPairs.product1", "$productPairs.product2"],
                    categories: ["$productPairs.category1", "$productPairs.category2"]
                },
                else: {
                    products: ["$productPairs.product2", "$productPairs.product1"],
                    categories: ["$productPairs.category2", "$productPairs.category1"]
                }
            }
        },
        pairRevenue: "$productPairs.revenue"
    }},
    
    // Analyser les associations
    { $group: {
        _id: "$pair",
        
        // Métriques d'association
        frequency: { $sum: 1 },
        totalRevenue: { $sum: "$pairRevenue" },
        avgRevenue: { $avg: "$pairRevenue" },
        
        // Contexte des paniers
        avgBasketSize: { $avg: "$basketDetails.totalItems" },
        basketsContainingPair: { $addToSet: "$_id" },
        
        // Analyse temporelle
        firstSeen: { $min: "$date" },
        lastSeen: { $max: "$date" },
        
        // Distribution par magasin
        storeDistribution: {
            $push: "$store.city"
        }
    }},
    
    // Calculer des métriques avancées
    { $project: {
        products: "$_id.products",
        categories: "$_id.categories",
        
        // KPIs d'association
        metrics: {
            frequency: "$frequency",
            totalRevenue: { $round: ["$totalRevenue", 2] },
            avgRevenue: { $round: ["$avgRevenue", 2] },
            avgBasketSize: { $round: ["$avgBasketSize", 1] }
        },
        
        // Score de confiance (simplifié)
        confidenceScore: {
            $round: [
                { $multiply: [
                    { $divide: ["$frequency", 100] },  // Normaliser
                    { $divide: ["$avgRevenue", 50] }   // Normaliser
                ]},
                3
            ]
        },
        
        // Analyse géographique
        topStores: {
            $slice: [
                { $sortArray: {
                    input: { 
                        $objectToArray: {
                            $reduce: {
                                input: "$storeDistribution",
                                initialValue: {},
                                in: {
                                    $mergeObjects: [
                                        "$$value",
                                        { 
                                            $literal: { 
                                                $concat: ["$$this", ""] 
                                            }: {
                                                $add: [
                                                    { $ifNull: ["$$value.$$this", 0] },
                                                    1
                                                ]
                                            }
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    sortBy: { v: -1 }
                }},
                3
            ]
        },
        
        // Suggestion de promotion
        promotionSuggestion: {
            $switch: {
                branches: [
                    {
                        case: { $gte: ["$frequency", 500] },
                        then: "Bundle haute fréquence - Réduction 10%"
                    },
                    {
                        case: { $and: [
                            { $gte: ["$avgRevenue", 100] },
                            { $gte: ["$frequency", 200] }
                        ]},
                        then: "Bundle premium - Réduction 15%"
                    },
                    {
                        case: { $gte: ["$frequency", 100] },
                        then: "Cross-sell opportunité"
                    }
                ],
                default: "Surveiller"
            }
        }
    }},
    
    // Top associations
    { $sort: { "metrics.frequency": -1 } },
    { $limit: 20 }
])

// Exemple de résultat :
{
    "products": ["Pain complet", "Beurre fermier"],
    "categories": ["Boulangerie", "Produits laitiers"],
    "metrics": {
        "frequency": 1234,
        "totalRevenue": 15420.50,
        "avgRevenue": 12.50,
        "avgBasketSize": 5.2
    },
    "confidenceScore": 2.468,
    "topStores": [
        { "k": "Casablanca", "v": 456 },
        { "k": "Rabat", "v": 389 },
        { "k": "Tanger", "v": 234 }
    ],
    "promotionSuggestion": "Bundle haute fréquence - Réduction 10%"
}
🎯 Applications pratiques :
  • Placement stratégique des produits en magasin
  • Création de bundles promotionnels
  • Recommandations personnalisées
  • Optimisation des stocks couplés

6 Dashboard Exécutif avec Window Functions Difficile

📋 Contexte

Le PDG de MarocMart veut un dashboard exécutif avec des KPIs avancés incluant des tendances et comparaisons.

🎯 Tâches

  1. Calculez le chiffre d'affaires cumulé par mois
  2. Comparez chaque mois avec le mois précédent (% croissance)
  3. Identifiez les tendances sur moyenne mobile 3 mois
  4. Classez les magasins par performance relative
💡 Indice : MongoDB 5.0+ offre $setWindowFields pour les window functions. Sinon, utilisez des techniques de self-lookup ou des calculs dans $group.

📖 Solution complète :

// Solution avec MongoDB 5.0+ (Window Functions)
db.sales.aggregate([
    // Agrégation mensuelle par magasin
    { $group: {
        _id: {
            year: { $year: "$date" },
            month: { $month: "$date" },
            storeId: "$store.id",
            storeName: "$store.name",
            storeCity: "$store.city"
        },
        monthlyRevenue: { $sum: "$total" },
        transactionCount: { $sum: 1 },
        avgTransaction: { $avg: "$total" },
        uniqueProducts: { $addToSet: "$items.product" }
    }},
    
    // Créer un champ date pour le tri
    { $addFields: {
        monthDate: {
            $dateFromParts: {
                year: "$_id.year",
                month: "$_id.month",
                day: 1
            }
        },
        productDiversity: { $size: "$uniqueProducts" }
    }},
    
    // Window Functions pour analyses avancées
    { $setWindowFields: {
        partitionBy: {
            storeId: "$_id.storeId"
        },
        sortBy: { monthDate: 1 },
        output: {
            // Chiffre d'affaires cumulé
            cumulativeRevenue: {
                $sum: "$monthlyRevenue",
                window: {
                    documents: ["unbounded", "current"]
                }
            },
            
            // Revenu du mois précédent
            previousMonthRevenue: {
                $shift: {
                    output: "$monthlyRevenue",
                    by: -1
                }
            },
            
            // Moyenne mobile 3 mois
            movingAvg3Months: {
                $avg: "$monthlyRevenue",
                window: {
                    documents: [-2, 0]
                }
            },
            
            // Rang dans le mois (tous magasins)
            monthlyRank: {
                $rank: {}
            }
        }
    }},
    
    // Calculer les métriques dérivées
    { $addFields: {
        // Croissance mois sur mois
        monthlyGrowth: {
            $cond: {
                if: { $gt: ["$previousMonthRevenue", 0] },
                then: {
                    $round: [
                        { $multiply: [
                            { $divide: [
                                { $subtract: ["$monthlyRevenue", "$previousMonthRevenue"] },
                                "$previousMonthRevenue"
                            ]},
                            100
                        ]},
                        2
                    ]
                },
                else: null
            }
        },
        
        // Écart par rapport à la moyenne mobile
        deviationFromTrend: {
            $round: [
                { $subtract: [
                    "$monthlyRevenue",
                    "$movingAvg3Months"
                ]},
                2
            ]
        },
        
        // Performance relative (base 100)
        performanceIndex: {
            $round: [
                { $multiply: [
                    { $divide: ["$monthlyRevenue", "$movingAvg3Months"] },
                    100
                ]},
                1
            ]
        }
    }},
    
    // Enrichir avec des statistiques globales
    { $facet: {
        // Données détaillées
        "detailsByStore": [
            { $sort: { "_id.storeId": 1, monthDate: 1 } },
            { $project: {
                _id: 0,
                store: {
                    id: "$_id.storeId",
                    name: "$_id.storeName",
                    city: "$_id.storeCity"
                },
                period: {
                    $dateToString: {
                        format: "%Y-%m",
                        date: "$monthDate"
                    }
                },
                revenue: {
                    monthly: { $round: ["$monthlyRevenue", 2] },
                    cumulative: { $round: ["$cumulativeRevenue", 2] },
                    movingAvg: { $round: ["$movingAvg3Months", 2] }
                },
                growth: {
                    percentage: "$monthlyGrowth",
                    trend: {
                        $switch: {
                            branches: [
                                { case: { $gt: ["$monthlyGrowth", 10] }, then: "↗️ Forte croissance" },
                                { case: { $gt: ["$monthlyGrowth", 0] }, then: "↗️ Croissance" },
                                { case: { $lt: ["$monthlyGrowth", -10] }, then: "↘️ Forte baisse" },
                                { case: { $lt: ["$monthlyGrowth", 0] }, then: "↘️ Baisse" }
                            ],
                            default: "→ Stable"
                        }
                    }
                },
                performance: {
                    index: "$performanceIndex",
                    rank: "$monthlyRank",
                    status: {
                        $switch: {
                            branches: [
                                { case: { $gte: ["$performanceIndex", 110] }, then: "🌟 Excellent" },
                                { case: { $gte: ["$performanceIndex", 100] }, then: "✅ Bon" },
                                { case: { $gte: ["$performanceIndex", 90] }, then: "⚠️ À surveiller" },
                                { case: { $lt: ["$performanceIndex", 90] }, then: "🔴 Critique" }
                            ]
                        }
                    }
                },
                operations: {
                    transactions: "$transactionCount",
                    avgTicket: { $round: ["$avgTransaction", 2] },
                    productDiversity: "$productDiversity"
                }
            }}
        ],
        
        // Résumé exécutif
        "executiveSummary": [
            { $group: {
                _id: null,
                totalRevenue2024: { $sum: "$monthlyRevenue" },
                avgMonthlyRevenue: { $avg: "$monthlyRevenue" },
                topPerformingMonth: { $max: {
                    revenue: "$monthlyRevenue",
                    month: "$monthDate"
                }},
                
                // Identifier les magasins stars
                storePerformances: {
                    $push: {
                        store: "$_id.storeName",
                        avgRevenue: "$monthlyRevenue",
                        lastGrowth: "$monthlyGrowth"
                    }
                }
            }},
            { $project: {
                _id: 0,
                year2024: {
                    total: { $round: ["$totalRevenue2024", 2] },
                    monthlyAverage: { $round: ["$avgMonthlyRevenue", 2] },
                    bestMonth: "$topPerformingMonth"
                },
                
                // Top 3 magasins
                topStores: {
                    $slice: [
                        { $sortArray: {
                            input: "$storePerformances",
                            sortBy: { avgRevenue: -1 }
                        }},
                        3
                    ]
                }
            }}
        ]
    }}
])

// Alternative sans Window Functions (MongoDB < 5.0)
// Utilisation de self-lookup pour calculer les métriques
db.sales.aggregate([
    // Étape 1 : Agrégation mensuelle
    { $group: {
        _id: {
            yearMonth: { 
                $dateToString: { 
                    format: "%Y-%m", 
                    date: "$date" 
                }
            },
            storeId: "$store.id",
            storeName: "$store.name"
        },
        revenue: { $sum: "$total" },
        transactions: { $sum: 1 }
    }},
    
    // Étape 2 : Self-lookup pour historique
    { $lookup: {
        from: "sales",
        let: { 
            currentMonth: "$_id.yearMonth",
            currentStore: "$_id.storeId"
        },
        pipeline: [
            { $match: {
                $expr: {
                    $and: [
                        { $eq: ["$store.id", "$$currentStore"] },
                        { $lte: [
                            { $dateToString: { 
                                format: "%Y-%m", 
                                date: "$date" 
                            }},
                            "$$currentMonth"
                        ]}
                    ]
                }
            }},
            { $group: {
                _id: { 
                    $dateToString: { 
                        format: "%Y-%m", 
                        date: "$date" 
                    }
                },
                revenue: { $sum: "$total" }
            }},
            { $sort: { _id: 1 } }
        ],
        as: "historicalData"
    }},
    
    // Étape 3 : Calculer les métriques cumulatives
    { $addFields: {
        cumulativeRevenue: { $sum: "$historicalData.revenue" },
        
        // Trouver le mois précédent
        previousMonth: {
            $let: {
                vars: {
                    sortedHistory: {
                        $filter: {
                            input: "$historicalData",
                            cond: { $lt: ["$$this._id", "$_id.yearMonth"] }
                        }
                    }
                },
                in: { $last: "$$sortedHistory" }
            }
        }
    }}
])
Exemple de dashboard exécutif :
{
    "detailsByStore": [
        {
            "store": {
                "id": "STR001",
                "name": "MarocMart Maarif",
                "city": "Casablanca"
            },
            "period": "2024-03",
            "revenue": {
                "monthly": 458750.50,
                "cumulative": 1234567.89,
                "movingAvg": 425000.00
            },
            "growth": {
                "percentage": 8.5,
                "trend": "↗️ Croissance"
            },
            "performance": {
                "index": 108,
                "rank": 2,
                "status": "✅ Bon"
            },
            "operations": {
                "transactions": 6543,
                "avgTicket": 70.15,
                "productDiversity": 234
            }
        }
    ],
    "executiveSummary": [{
        "year2024": {
            "total": 15678900.50,
            "monthlyAverage": 1306575.04,
            "bestMonth": {
                "revenue": 1890000.00,
                "month": "2024-12"
            }
        },
        "topStores": [
            { "store": "MarocMart Marina", "avgRevenue": 567000 },
            { "store": "MarocMart Maarif", "avgRevenue": 458000 },
            { "store": "MarocMart Anfa", "avgRevenue": 445000 }
        ]
    }]
}

7 Projet Final : Système de Recommandation Difficile

📋 Contexte

Créez un système de recommandation complet pour MarocMart qui suggère des produits personnalisés aux clients.

🎯 Tâches

  1. Analysez l'historique d'achat du client
  2. Identifiez les patterns d'achat similaires
  3. Générez des recommandations personnalisées
  4. Calculez un score de pertinence pour chaque recommandation
💡 Indice : Combinez collaborative filtering (clients similaires) et content-based filtering (produits similaires) pour des recommandations robustes.

📖 Solution complète :

// Système de recommandation hybride
function getRecommendations(customerId) {
    return db.sales.aggregate([
        // Phase 1 : Profil du client cible
        { $facet: {
            // Historique d'achat du client
            "customerProfile": [
                { $match: { customerId: customerId } },
                { $unwind: "$items" },
                { $group: {
                    _id: null,
                    purchasedProducts: { 
                        $addToSet: "$items.product" 
                    },
                    favoriteCategories: {
                        $push: "$items.category"
                    },
                    avgSpending: { $avg: "$total" },
                    totalSpent: { $sum: "$total" },
                    purchaseCount: { $sum: 1 },
                    lastPurchase: { $max: "$date" }
                }},
                { $addFields: {
                    // Top 3 catégories
                    topCategories: {
                        $slice: [
                            { $sortArray: {
                                input: { 
                                    $reduce: {
                                        input: "$favoriteCategories",
                                        initialValue: [],
                                        in: {
                                            $concatArrays: [
                                                { $filter: {
                                                    input: "$$value",
                                                    cond: { 
                                                        $ne: ["$$this.category", "$$this"] 
                                                    }
                                                }},
                                                [{
                                                    category: "$$this",
                                                    count: {
                                                        $size: {
                                                            $filter: {
                                                                input: "$favoriteCategories",
                                                                cond: { 
                                                                    $eq: ["$$this", "$$this"] 
                                                                }
                                                            }
                                                        }
                                                    }
                                                }]
                                            ]
                                        }
                                    }
                                },
                                sortBy: { count: -1 }
                            }},
                            3
                        ]
                    }
                }}
            ],
            
            // Clients similaires (Collaborative Filtering)
            "similarCustomers": [
                { $match: { customerId: customerId } },
                { $unwind: "$items" },
                { $group: {
                    _id: "$items.product",
                    buyers: { $addToSet: "$customerId" }
                }},
                // Trouver qui a acheté les mêmes produits
                { $lookup: {
                    from: "sales",
                    let: { product: "$_id" },
                    pipeline: [
                        { $unwind: "$items" },
                        { $match: {
                            $expr: {
                                $and: [
                                    { $eq: ["$items.product", "$$product"] },
                                    { $ne: ["$customerId", customerId] }
                                ]
                            }
                        }},
                        { $group: {
                            _id: "$customerId",
                            commonProducts: { $sum: 1 }
                        }}
                    ],
                    as: "otherBuyers"
                }},
                { $unwind: "$otherBuyers" },
                { $group: {
                    _id: "$otherBuyers._id",
                    similarity: { $sum: "$otherBuyers.commonProducts" }
                }},
                { $sort: { similarity: -1 } },
                { $limit: 50 }  // Top 50 clients similaires
            ]
        }},
        
        // Phase 2 : Générer les recommandations
        { $project: {
            customerData: { $first: "$customerProfile" },
            similarCustomerIds: "$similarCustomers._id"
        }},
        
        // Produits achetés par clients similaires
        { $lookup: {
            from: "sales",
            let: { 
                similarIds: "$similarCustomerIds",
                purchasedProducts: "$customerData.purchasedProducts"
            },
            pipeline: [
                { $match: {
                    $expr: { 
                        $in: ["$customerId", "$$similarIds"] 
                    }
                }},
                { $unwind: "$items" },
                // Exclure les produits déjà achetés
                { $match: {
                    $expr: {
                        $not: {
                            $in: ["$items.product", "$$purchasedProducts"]
                        }
                    }
                }},
                { $group: {
                    _id: {
                        product: "$items.product",
                        category: "$items.category"
                    },
                    purchaseCount: { $sum: 1 },
                    avgPrice: { $avg: "$items.price" },
                    buyers: { $addToSet: "$customerId" }
                }}
            ],
            as: "candidateProducts"
        }},
        
        // Phase 3 : Scorer et classer les recommandations
        { $unwind: "$candidateProducts" },
        { $project: {
            product: "$candidateProducts._id.product",
            category: "$candidateProducts._id.category",
            
            // Calcul du score de recommandation
            recommendationScore: {
                $add: [
                    // Score de popularité (0-40 points)
                    { $multiply: [
                        { $min: [
                            { $divide: ["$candidateProducts.purchaseCount", 10] },
                            1
                        ]},
                        40
                    ]},
                    
                    // Score de catégorie (0-30 points)
                    { $cond: {
                        if: { 
                            $in: [
                                "$candidateProducts._id.category", 
                                "$customerData.topCategories.category"
                            ]
                        },
                        then: 30,
                        else: 10
                    }},
                    
                    // Score de prix (0-20 points)
                    { $multiply: [
                        { $abs: {
                            $subtract: [
                                1,
                                { $divide: [
                                    { $abs: {
                                        $subtract: [
                                            "$candidateProducts.avgPrice",
                                            "$customerData.avgSpending"
                                        ]
                                    }},
                                    "$customerData.avgSpending"
                                ]}
                            ]
                        }},
                        20
                    ]},
                    
                    // Score de diversité (0-10 points)
                    { $cond: {
                        if: { $gte: [
                            { $size: "$candidateProducts.buyers" },
                            5
                        ]},
                        then: 10,
                        else: 5
                    }}
                ]
            },
            
            // Métadonnées pour explication
            metadata: {
                popularityScore: "$candidateProducts.purchaseCount",
                buyerCount: { $size: "$candidateProducts.buyers" },
                avgPrice: { $round: ["$candidateProducts.avgPrice", 2] },
                inFavoriteCategory: {
                    $in: [
                        "$candidateProducts._id.category", 
                        "$customerData.topCategories.category"
                    ]
                }
            }
        }},
        
        // Enrichir avec détails produits
        { $lookup: {
            from: "products",
            localField: "product",
            foreignField: "name",
            as: "productDetails"
        }},
        { $unwind: {
            path: "$productDetails",
            preserveNullAndEmptyArrays: true
        }},
        
        // Finaliser les recommandations
        { $project: {
            productName: "$product",
            category: "$category",
            score: { $round: ["$recommendationScore", 1] },
            confidence: {
                $switch: {
                    branches: [
                        { case: { $gte: ["$recommendationScore", 80] }, then: "Très élevée" },
                        { case: { $gte: ["$recommendationScore", 60] }, then: "Élevée" },
                        { case: { $gte: ["$recommendationScore", 40] }, then: "Moyenne" }
                    ],
                    default: "Faible"
                }
            },
            reason: {
                $concat: [
                    "Acheté par ",
                    { $toString: "$metadata.buyerCount" },
                    " clients similaires",
                    { $cond: {
                        if: "$metadata.inFavoriteCategory",
                        then: " • Dans vos catégories préférées",
                        else: ""
                    }}
                ]
            },
            price: "$metadata.avgPrice",
            inStock: { $ifNull: ["$productDetails.stock", "N/A"] }
        }},
        
        // Top 10 recommandations
        { $sort: { score: -1 } },
        { $limit: 10 }
    ], { allowDiskUse: true });
}

// Utilisation
const recommendations = getRecommendations("CUST001");

// Résultat exemple :
[
    {
        "productName": "Huile d'argan bio",
        "category": "Épicerie",
        "score": 85.5,
        "confidence": "Très élevée",
        "reason": "Acheté par 23 clients similaires • Dans vos catégories préférées",
        "price": 89.90,
        "inStock": 45
    },
    {
        "productName": "Thé vert à la menthe Premium",
        "category": "Boissons",
        "score": 78.2,
        "confidence": "Élevée",
        "reason": "Acheté par 18 clients similaires • Dans vos catégories préférées",
        "price": 34.50,
        "inStock": 120
    }
    // ...
]
🚀 Améliorations possibles :
  • Ajouter la saisonnalité (produits de saison)
  • Intégrer les promotions en cours
  • Personnaliser selon l'heure/jour
  • A/B testing des algorithmes
  • Feedback loop pour améliorer les scores

📈 Progression