Tableaux de bord BI pour PME avec Python : guide pratique

Quand je commence une mission chez un client PME, la situation est souvent la même : les données existent, mais personne ne les exploite. Il y a des fichiers Excel partout, des exports CSV qu'on copie-colle dans d'autres Excel, des tableaux croisés dynamiques que seul Jean-Pierre de la compta sait refaire, et un ERP qui contient 5 ans de données que personne ne regarde jamais.

La Business Intelligence, ça sonne corporate et cher. Mais en réalité, pour une PME de 20 à 200 salariés, on peut mettre en place un système de pilotage décisionnel efficace avec des outils open source et un budget raisonnable. Voici comment.

Ce que la BI résout concrètement dans une PME

Avant de parler technique, posons le problème. Dans une PME typique, les décisions se prennent au feeling ou sur des chiffres qui datent du mois dernier. Le dirigeant demande "on en est où sur le CA ?", et quelqu'un passe 45 minutes à compiler un tableau. Le responsable logistique ne sait pas quel produit a le meilleur taux de rotation. Le commercial ne peut pas dire quel client n'a pas commandé depuis 3 mois.

Un tableau de bord BI bien fait répond à ces questions en temps réel, automatiquement, sans intervention humaine. C'est du temps gagné, mais surtout des décisions prises sur des faits plutôt que sur l'intuition.

Voici ce que j'ai mis en place chez des clients réels :

  • Logistique (transport routier, 80 salariés) : dashboard suivi des marges par trajet, taux de remplissage, coût au kilomètre. Résultat : identification de 3 lignes déficitaires en 2 semaines, économie estimée 45 000 €/an.
  • Distribution (négoce B2B, 35 salariés) : tableau de bord commercial avec suivi des ventes par commercial, par zone, par famille produit. Le dirigeant voyait ses chiffres le matin en arrivant au lieu d'attendre le reporting mensuel.
  • Industrie (sous-traitant mécanique, 120 salariés) : suivi de production avec TRS (Taux de Rendement Synthétique), taux de rebut, temps de changement de série. Amélioration du TRS de 62% à 71% en 6 mois.

L'architecture technique : simple et efficace

Oubliez les usines à gaz SAP BO ou les licences Tableau à 70 €/mois par utilisateur. Pour une PME, l'architecture BI se résume à 3 couches :

1. Extraction des données (ETL)

La première étape, c'est de sortir les données de là où elles sont. Dans une PME, ça veut dire : l'ERP (Sage, EBP, Cegid, parfois un AS/400), des fichiers Excel, une base Access que personne n'ose toucher, et peut-être un CRM (Salesforce, HubSpot, ou un fichier Excel qui fait office de CRM).

En Python, on utilise des connecteurs adaptés à chaque source :

# etl_pipeline.py — extraction multi-source
import pandas as pd
import pyodbc
from pathlib import Path

def extraire_ventes_erp(connexion_string: str) -> pd.DataFrame:
    """Extraction des ventes depuis l'ERP (SQL Server / Sage)."""
    query = """
        SELECT
            f.FA_DATFAC AS date_facture,
            f.FA_NUMFAC AS num_facture,
            c.CT_INTITULE AS client,
            c.CT_CODEREGION AS region,
            l.DL_DESIGN AS produit,
            l.DL_QTEFAC AS quantite,
            l.DL_MONTANTHT AS montant_ht,
            l.FA_CODEREP AS commercial
        FROM F_DOCLIGNE l
        JOIN F_DOCENTETE f ON l.DO_PIECE = f.DO_PIECE
        JOIN F_COMPTET c ON f.DO_TIERS = c.CT_NUM
        WHERE f.DO_TYPE = 6  -- Factures uniquement
          AND f.FA_DATFAC >= DATEADD(month, -12, GETDATE())
    """
    conn = pyodbc.connect(connexion_string)
    df = pd.read_sql(query, conn)
    conn.close()
    return df


def extraire_stocks_excel(chemin: str) -> pd.DataFrame:
    """Extraction des stocks depuis le fichier Excel mensuel."""
    df = pd.read_excel(
        chemin,
        sheet_name='Stocks',
        usecols=['Reference', 'Designation', 'Qte_Stock', 'PMP', 'Emplacement'],
    )
    df.columns = ['reference', 'designation', 'quantite', 'prix_moyen', 'emplacement']
    df['valeur_stock'] = df['quantite'] * df['prix_moyen']
    return df


def pipeline_quotidien():
    """Pipeline ETL complet — exécuté chaque nuit à 3h."""
    ventes = extraire_ventes_erp("DRIVER={ODBC Driver 17};SERVER=srv-erp;...")
    stocks = extraire_stocks_excel("/partage/compta/stocks_202603.xlsx")

    # Nettoyage
    ventes['date_facture'] = pd.to_datetime(ventes['date_facture'])
    ventes['mois'] = ventes['date_facture'].dt.to_period('M')

    # Agrégations
    ca_mensuel = ventes.groupby('mois')['montant_ht'].sum()
    ca_par_commercial = ventes.groupby('commercial')['montant_ht'].sum()
    top_clients = ventes.groupby('client')['montant_ht'].sum().nlargest(20)

    return {
        'ventes': ventes,
        'stocks': stocks,
        'ca_mensuel': ca_mensuel,
        'ca_par_commercial': ca_par_commercial,
        'top_clients': top_clients,
    }

Le pipeline tourne en tâche planifiée (cron ou Celery) chaque nuit. Les données sont nettoyées, transformées et stockées dans une base PostgreSQL dédiée — le "data warehouse" de la PME, même si on n'utilise pas ce terme pour ne pas effrayer le comptable.

2. Stockage et modélisation

Pour une PME, PostgreSQL suffit largement. On structure les données en tables analytiques, avec un schéma en étoile simplifié :

# models.py — tables analytiques Django
from django.db import models


class FactVente(models.Model):
    """Table de faits : une ligne par ligne de facture."""
    date_facture = models.DateField(db_index=True)
    num_facture = models.CharField(max_length=20)
    client = models.CharField(max_length=200)
    region = models.CharField(max_length=50, blank=True)
    produit = models.CharField(max_length=200)
    quantite = models.DecimalField(max_digits=12, decimal_places=3)
    montant_ht = models.DecimalField(max_digits=12, decimal_places=2)
    commercial = models.CharField(max_length=100, blank=True)
    mois = models.CharField(max_length=7, db_index=True)  # 2026-03

    class Meta:
        indexes = [
            models.Index(fields=['mois', 'commercial']),
            models.Index(fields=['client', 'date_facture']),
        ]


class IndicateurJour(models.Model):
    """Table agrégée : un KPI par jour."""
    date = models.DateField(unique=True)
    ca_jour = models.DecimalField(max_digits=12, decimal_places=2, default=0)
    nb_factures = models.PositiveIntegerField(default=0)
    panier_moyen = models.DecimalField(max_digits=10, decimal_places=2, default=0)
    nb_nouveaux_clients = models.PositiveIntegerField(default=0)
    valeur_stock = models.DecimalField(max_digits=14, decimal_places=2, default=0)

L'avantage de passer par Django : on a l'ORM pour les requêtes, l'admin pour la maintenance, et les migrations pour versionner le schéma. Pas besoin d'un outil supplémentaire.

3. Visualisation : le tableau de bord

C'est la partie visible. Deux approches selon le contexte :

Option A — Plotly/Dash intégré à Django. On crée des graphiques interactifs directement dans l'application web. Le dirigeant ouvre son navigateur, il voit ses chiffres. Pas d'outil supplémentaire à installer, pas de licence.

# views.py — API pour le dashboard
from django.http import JsonResponse
from django.db.models import Sum, Count
from django.db.models.functions import TruncMonth

from .models import FactVente


def api_ca_mensuel(request):
    """CA mensuel sur les 12 derniers mois."""
    data = (
        FactVente.objects
        .values('mois')
        .annotate(ca=Sum('montant_ht'), nb=Count('num_facture'))
        .order_by('mois')
    )
    return JsonResponse({
        'labels': [d['mois'] for d in data],
        'ca': [float(d['ca']) for d in data],
        'nb_factures': [d['nb'] for d in data],
    })


def api_top_clients(request):
    """Top 10 clients par CA."""
    data = (
        FactVente.objects
        .values('client')
        .annotate(ca=Sum('montant_ht'))
        .order_by('-ca')[:10]
    )
    return JsonResponse({
        'clients': [d['client'] for d in data],
        'ca': [float(d['ca']) for d in data],
    })

Option B — Power BI / Metabase connecté à PostgreSQL. Si le client a déjà des licences Microsoft 365, Power BI Desktop est gratuit. On le connecte directement à la base PostgreSQL et on construit les rapports visuellement. Pour les PME sans écosystème Microsoft, Metabase est une alternative open source très solide : installation en 5 minutes via Docker, interface intuitive, et les utilisateurs métier peuvent créer leurs propres requêtes sans écrire de SQL.

Combien ça coûte concrètement

Soyons transparents sur les chiffres. Voici ce que coûte un projet BI typique en PME, basé sur mes missions récentes :

PosteFourchetteDétail
Cadrage et audit données1 500 - 3 000 €2-4 jours : inventaire sources, qualité données, définition KPI
ETL + modélisation3 000 - 8 000 €5-15 jours : connecteurs, nettoyage, base analytique
Dashboard (3-5 écrans)2 000 - 5 000 €4-10 jours : graphiques, filtres, responsive
Formation utilisateurs500 - 1 000 €1-2 jours : prise en main, autonomie
Total projet7 000 - 17 000 €Hors hébergement (serveur existant ou VPS 20 €/mois)

Comparé aux alternatives :

  • Tableau : 70 €/mois/utilisateur × 10 users = 8 400 €/an rien qu'en licences, sans le développement
  • Power BI Pro : 9,40 €/mois/utilisateur, mais nécessite Microsoft 365 E5 pour les fonctions avancées
  • Solution custom Python/Django : 0 € de licence, coût = développement + maintenance (~1 jour/mois)

Sur 3 ans, la solution custom revient presque toujours moins cher qu'un SaaS pour une PME qui a entre 5 et 20 utilisateurs du dashboard.

Les erreurs classiques (et comment les éviter)

Erreur 1 : vouloir tout mesurer

Le réflexe du dirigeant, c'est de demander 47 indicateurs. Le résultat : un dashboard que personne ne regarde parce qu'il est illisible. La règle que j'applique : 5 à 7 KPI maximum par écran, et un maximum de 3 écrans. Si un indicateur ne déclenche pas une action concrète, on le vire.

Exemples de bons KPI pour une PME :

  • CA du mois vs objectif (et vs même mois N-1)
  • Marge brute par famille de produits
  • Délai moyen de paiement client (DSO)
  • Taux de service / livraison à temps
  • Top 5 clients en croissance / en déclin

Erreur 2 : ignorer la qualité des données

Garbage in, garbage out. Si les commerciaux saisissent "DUPONT", "Dupont SA", "DUPONT S.A." et "dupont" pour le même client, votre dashboard affichera 4 clients différents. Avant de construire quoi que ce soit, on passe par une phase de nettoyage et de normalisation. C'est ingrat mais indispensable.

# nettoyage.py — normalisation des noms clients
import re
import unicodedata


def normaliser_nom_client(nom: str) -> str:
    """Normalise un nom client pour éviter les doublons."""
    # Majuscules, suppression accents
    nom = nom.upper().strip()
    nom = unicodedata.normalize('NFD', nom)
    nom = nom.encode('ascii', 'ignore').decode('ascii')

    # Suppression formes juridiques
    formes = ['SAS', 'SARL', 'SA', 'SCI', 'EURL', 'S.A.', 'S.A.S.']
    for forme in formes:
        nom = re.sub(rf'\b{re.escape(forme)}\b', '', nom)

    # Nettoyage final
    nom = re.sub(r'[^A-Z0-9 ]', '', nom)
    nom = re.sub(r'\s+', ' ', nom).strip()
    return nom


# "DUPONT S.A." → "DUPONT"
# "dupont sa"   → "DUPONT"
# "Dupont SAS"  → "DUPONT"

Erreur 3 : oublier le mobile

Le dirigeant d'une PME n'est pas derrière un écran 27 pouces. Il regarde ses chiffres sur son téléphone entre deux rendez-vous. Si le dashboard n'est pas lisible sur mobile, il ne sera pas utilisé. C'est aussi simple que ça.

Erreur 4 : ne pas automatiser les alertes

Un bon système BI ne se contente pas d'afficher des graphiques. Il envoie des alertes quand un seuil est franchi : stock critique, impayé > 60 jours, CA en baisse de plus de 15% sur un mois. L'alerte part par email ou Slack, et le décideur agit sans avoir besoin d'ouvrir le dashboard.

# alertes.py — alertes automatiques par email
from django.core.mail import send_mail
from django.db.models import Sum, Q
from datetime import date, timedelta

from .models import FactVente


def verifier_alertes_quotidiennes():
    """Vérifie les seuils et envoie les alertes."""
    aujourd_hui = date.today()
    debut_mois = aujourd_hui.replace(day=1)
    meme_mois_n1 = debut_mois.replace(year=debut_mois.year - 1)

    # CA mois en cours vs N-1
    ca_mois = FactVente.objects.filter(
        date_facture__gte=debut_mois
    ).aggregate(total=Sum('montant_ht'))['total'] or 0

    ca_mois_n1 = FactVente.objects.filter(
        date_facture__gte=meme_mois_n1,
        date_facture__lt=meme_mois_n1 + timedelta(days=31),
    ).aggregate(total=Sum('montant_ht'))['total'] or 0

    if ca_mois_n1 > 0:
        variation = (ca_mois - ca_mois_n1) / ca_mois_n1 * 100
        if variation < -15:
            send_mail(
                subject=f"Alerte CA : {variation:+.1f}% vs N-1",
                message=(
                    f"CA mois en cours : {ca_mois:,.0f} EUR\n"
                    f"CA même mois N-1 : {ca_mois_n1:,.0f} EUR\n"
                    f"Variation : {variation:+.1f}%"
                ),
                from_email="bi@goodev.fr",
                recipient_list=["direction@client.fr"],
            )

    # Clients inactifs > 90 jours
    seuil = aujourd_hui - timedelta(days=90)
    clients_inactifs = (
        FactVente.objects
        .values('client')
        .annotate(derniere_facture=models.Max('date_facture'))
        .filter(derniere_facture__lt=seuil)
    )

    if clients_inactifs.count() > 0:
        liste = "\n".join(
            f"- {c['client']} (dernière facture : {c['derniere_facture']})"
            for c in clients_inactifs[:10]
        )
        send_mail(
            subject=f"{clients_inactifs.count()} clients inactifs > 90 jours",
            message=f"Clients sans commande depuis plus de 90 jours :\n\n{liste}",
            from_email="bi@goodev.fr",
            recipient_list=["commercial@client.fr"],
        )

Cas réel : PME logistique, 80 salariés

Pour illustrer, voici un projet que j'ai mené de bout en bout.

Le contexte : une entreprise de transport routier en Grand-Est. 80 salariés, 45 camions, 12 000 trajets/an. Le dirigeant pilotait son activité avec un fichier Excel de 15 onglets qu'un contrôleur de gestion mettait à jour à la main chaque vendredi. Les marges par trajet étaient calculées une fois par trimestre — trop tard pour réagir.

Ce qu'on a fait :

  1. Audit données (3 jours) : inventaire des sources (TMS Akanea, comptabilité Sage, fichiers péages Eurotoll, relevés carburant). Identification des 7 KPI critiques.
  2. ETL Python (8 jours) : connecteurs pour chaque source, normalisation, calcul des marges par trajet (recette - gasoil - péages - salaire chauffeur proratisé - amortissement véhicule).
  3. Dashboard Django (6 jours) : 3 écrans — vue direction (CA, marge globale, trésorerie), vue exploitation (marge par trajet, taux remplissage, km à vide), vue commercial (CA par client, impayés).
  4. Alertes (2 jours) : email quotidien si marge < 8% sur un trajet, alerte stock gasoil, notification impayé > 45 jours.
  5. Formation (1 jour) : prise en main par le dirigeant, le contrôleur de gestion et 2 exploitants.

Budget total : 14 000 € HT (20 jours de prestation).

Résultats après 6 mois :

  • Identification de 3 lignes systématiquement déficitaires (marge < 3%), renégociées ou arrêtées → économie estimée 45 000 €/an
  • Réduction du taux de km à vide de 23% à 17% grâce à une meilleure visibilité sur les retours
  • Gain de temps : le reporting hebdo est passé de 4h à 0h (automatisé). Le contrôleur de gestion a pu se concentrer sur l'analyse plutôt que la saisie
  • ROI du projet : le budget BI (14K €) a été amorti en moins de 4 mois

Par où commencer

Si vous êtes dirigeant de PME et que vous lisez cet article en vous disant "c'est exactement mon problème", voici les 3 premières étapes :

  1. Listez vos 5 questions business récurrentes. "Quel est mon CA du mois ?", "Quel commercial performe le plus ?", "Quels produits ont la meilleure marge ?" — ces questions deviennent vos KPI.
  2. Inventoriez vos sources de données. ERP, Excel, CRM, fichiers fournisseurs. Notez le format, la fréquence de mise à jour, et qui est responsable de chaque source.
  3. Commencez petit. Un seul dashboard avec 5 KPI, connecté à votre source principale (l'ERP en général). Mieux vaut un outil simple que tout le monde utilise qu'un système complet que personne ne regarde.

La BI n'est pas réservée aux grands groupes avec des budgets à 6 chiffres. Avec Python, PostgreSQL et les bons outils, une PME peut avoir un pilotage décisionnel de qualité pour un investissement raisonnable. Le retour sur investissement est quasi systématique — j'ai rarement vu un projet BI en PME qui ne s'amortissait pas en moins d'un an.

Si vous avez un projet de tableau de bord ou de pilotage décisionnel, parlons-en. Un premier échange permet de valider la faisabilité et d'estimer le budget en quelques jours.

Besoin de tableaux de bord ?

Pipelines ETL, tableaux de bord interactifs et analyse de donnees pour votre activite.

En savoir plus →