Endpoints d'agrégation
Les endpoints d'agrégation ne retournent pas des lignes individuelles, mais des données calculées : totaux, moyennes, comptages, regroupements.
CRUD vs Agrégation
| Aspect | Endpoint CRUD | Endpoint d'agrégation |
|---|---|---|
| Retourne | Des lignes individuelles | Des valeurs calculées |
| Exemple | GET /api/v1/ventes (liste de ventes) | GET /api/v1/ventes/stats (totaux, moyennes) |
| Volume de réponse | Peut être très grand (pagination nécessaire) | Généralement compact |
| SQL typique | SELECT * FROM ventes | SELECT SUM(montant), COUNT(*) FROM ventes GROUP BY region |
| Usage | Afficher un tableau de données | Alimenter un dashboard, un graphique |
Cas d'usage typiques
Types d'agrégation courants :
- Totaux : chiffre d'affaires total, nombre de ventes
- Moyennes : panier moyen, montant moyen par transaction
- Comptages : nombre de clients actifs, nombre de commandes par jour
- Regroupements : ventes par région, CA mensuel, répartition par catégorie
- Évolutions : tendance mensuelle, comparaison année N vs N-1
Pensez dashboard
Chaque graphique d'un tableau de bord correspond à un endpoint d'agrégation. Un graphique en barres "CA par région" consomme un endpoint /ventes/par-region. Une courbe "évolution mensuelle" consomme /ventes/evolution-mensuelle.
Design des URI
Les endpoints d'agrégation sont des sous-ressources de la collection principale :
GET /api/v1/ventes/stats # Statistiques globales
GET /api/v1/ventes/par-region # Regroupement par région
GET /api/v1/ventes/par-categorie # Regroupement par catégorie
GET /api/v1/ventes/evolution-mensuelle # Série temporelle
GET /api/v1/ventes/top-produits # Classement
Les mêmes filtres que l'endpoint principal peuvent s'appliquer :
GET /api/v1/ventes/par-region?annee=2024
GET /api/v1/ventes/evolution-mensuelle?region=IDF
Implémentation avec SQLite
Statistiques globales
@ventes_bp.route('/stats', methods=['GET'])
def statistiques():
"""Statistiques globales des ventes."""
region = request.args.get('region')
annee = request.args.get('annee', type=int)
stats = repository.get_stats(region=region, annee=annee)
return jsonify(stats)
# Dans le Repository
def get_stats(self, region=None, annee=None):
"""Retourne les statistiques agrégées."""
conn = self._get_connection()
query = """
SELECT
COUNT(*) as nombre_ventes,
COALESCE(SUM(montant), 0) as chiffre_affaires_total,
COALESCE(AVG(montant), 0) as montant_moyen,
COALESCE(MIN(montant), 0) as montant_min,
COALESCE(MAX(montant), 0) as montant_max
FROM ventes
WHERE 1=1
"""
params = []
if region:
query += " AND region = ?"
params.append(region)
if annee:
query += " AND strftime('%Y', date) = ?"
params.append(str(annee))
result = conn.execute(query, params).fetchone()
conn.close()
return {
"nombre_ventes": result['nombre_ventes'],
"chiffre_affaires_total": round(result['chiffre_affaires_total'], 2),
"montant_moyen": round(result['montant_moyen'], 2),
"montant_min": result['montant_min'],
"montant_max": result['montant_max'],
}
COALESCE
COALESCE(valeur, 0) retourne 0 si la valeur est NULL (par exemple si la table est vide). Sans cela, la réponse JSON contiendrait null au lieu de 0, ce qui peut casser les graphiques côté client.
{
"nombre_ventes": 15420,
"chiffre_affaires_total": 2845600.50,
"montant_moyen": 184.54,
"montant_min": 5.99,
"montant_max": 12500.00
}
Regroupement par dimension (GROUP BY)
On regroupe les données selon une dimension (région, catégorie, mois) et on calcule des métriques pour chaque groupe.
@ventes_bp.route('/par-region', methods=['GET'])
def ventes_par_region():
"""Ventes agrégées par région."""
annee = request.args.get('annee', type=int)
result = repository.get_par_region(annee=annee)
return jsonify({"data": result})
# Dans le Repository
def get_par_region(self, annee=None):
"""Ventes groupées par région."""
conn = self._get_connection()
query = """
SELECT
region,
COUNT(*) as nombre_ventes,
SUM(montant) as chiffre_affaires,
AVG(montant) as panier_moyen
FROM ventes
WHERE 1=1
"""
params = []
if annee:
query += " AND strftime('%Y', date) = ?"
params.append(str(annee))
query += " GROUP BY region ORDER BY chiffre_affaires DESC"
rows = conn.execute(query, params).fetchall()
conn.close()
return [
{
"region": row['region'],
"nombre_ventes": row['nombre_ventes'],
"chiffre_affaires": round(row['chiffre_affaires'], 2),
"panier_moyen": round(row['panier_moyen'], 2),
}
for row in rows
]
{
"data": [
{"region": "IDF", "nombre_ventes": 5200, "chiffre_affaires": 1250000.00, "panier_moyen": 240.38},
{"region": "PACA", "nombre_ventes": 3100, "chiffre_affaires": 680000.00, "panier_moyen": 219.35},
{"region": "ARA", "nombre_ventes": 2800, "chiffre_affaires": 520000.00, "panier_moyen": 185.71}
]
}
Évolution temporelle (série chronologique)
Les séries chronologiques alimentent les courbes d'évolution.
@ventes_bp.route('/evolution-mensuelle', methods=['GET'])
def evolution_mensuelle():
"""Évolution mensuelle du chiffre d'affaires."""
region = request.args.get('region')
annee = request.args.get('annee', type=int)
result = repository.get_evolution_mensuelle(region=region, annee=annee)
return jsonify({"data": result})
# Dans le Repository
def get_evolution_mensuelle(self, region=None, annee=None):
"""Évolution mensuelle des ventes."""
conn = self._get_connection()
query = """
SELECT
strftime('%Y-%m', date) as mois,
COUNT(*) as nombre_ventes,
SUM(montant) as chiffre_affaires
FROM ventes
WHERE 1=1
"""
params = []
if region:
query += " AND region = ?"
params.append(region)
if annee:
query += " AND strftime('%Y', date) = ?"
params.append(str(annee))
query += " GROUP BY mois ORDER BY mois ASC"
rows = conn.execute(query, params).fetchall()
conn.close()
return [
{
"mois": row['mois'],
"nombre_ventes": row['nombre_ventes'],
"chiffre_affaires": round(row['chiffre_affaires'], 2),
}
for row in rows
]
{
"data": [
{"mois": "2024-01", "nombre_ventes": 1200, "chiffre_affaires": 215000.00},
{"mois": "2024-02", "nombre_ventes": 1350, "chiffre_affaires": 243000.00},
{"mois": "2024-03", "nombre_ventes": 1580, "chiffre_affaires": 298000.00}
]
}
Alternative : pandas pour des agrégations complexes
Pour des calculs plus complexes (percentiles, corrélations, pivots), pandas peut compléter SQL :
import pandas as pd
import sqlite3
def get_analyse_croisee(self, annee=None):
"""Analyse croisée région × catégorie avec pandas."""
conn = sqlite3.connect(self.db_path)
query = "SELECT region, categorie, montant FROM ventes"
params = []
if annee:
query += " WHERE strftime('%Y', date) = ?"
params.append(str(annee))
# Charger dans un DataFrame
df = pd.read_sql_query(query, conn, params=params)
conn.close()
# Tableau croisé dynamique
pivot = df.pivot_table(
values='montant',
index='region',
columns='categorie',
aggfunc=['sum', 'count'],
fill_value=0
)
# Convertir en format JSON-friendly
result = []
for region in pivot.index:
entry = {"region": region}
for categorie in df['categorie'].unique():
entry[f"{categorie}_ca"] = round(float(pivot.loc[region, ('sum', 'montant', categorie)]), 2)
entry[f"{categorie}_nb"] = int(pivot.loc[region, ('count', 'montant', categorie)])
result.append(entry)
return result
pandas dans une API
pandas dans une API convient pour des volumes modérés (quelques dizaines de milliers de lignes). Pour de très grands volumes, les requêtes SQL directes consomment moins de mémoire.
Structure des réponses d'agrégation
Les réponses d'agrégation n'ont généralement pas besoin de pagination (le résultat est compact). Structure recommandée :
{
"data": [
{"region": "IDF", "chiffre_affaires": 1250000.00},
{"region": "PACA", "chiffre_affaires": 680000.00}
]
}
Pour les statistiques globales (un seul objet), pas besoin de tableau :
{
"nombre_ventes": 15420,
"chiffre_affaires_total": 2845600.50,
"montant_moyen": 184.54
}
Quelle fonction SQL permet de calculer la somme des montants regroupés par région ?
Pourquoi utiliser COALESCE dans les requêtes d'agrégation ?
À retenir
Points clés
- Les endpoints d'agrégation retournent des données calculées (SUM, COUNT, AVG, GROUP BY)
- Ils sont conçus comme des sous-ressources :
/ventes/stats,/ventes/par-region - Les mêmes filtres que l'endpoint principal s'appliquent (région, année, etc.)
- Utilisez
COALESCEpour éviter les valeursNULLdans les réponses - pandas peut compléter SQL pour des agrégations complexes (pivots, percentiles)
- Les réponses d'agrégation sont généralement compactes et n'ont pas besoin de pagination
- Chaque graphique d'un dashboard correspond à un endpoint d'agrégation