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 :
| Poste | Fourchette | Détail |
|---|---|---|
| Cadrage et audit données | 1 500 - 3 000 € | 2-4 jours : inventaire sources, qualité données, définition KPI |
| ETL + modélisation | 3 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 utilisateurs | 500 - 1 000 € | 1-2 jours : prise en main, autonomie |
| Total projet | 7 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 :
- Audit données (3 jours) : inventaire des sources (TMS Akanea, comptabilité Sage, fichiers péages Eurotoll, relevés carburant). Identification des 7 KPI critiques.
- ETL Python (8 jours) : connecteurs pour chaque source, normalisation, calcul des marges par trajet (recette - gasoil - péages - salaire chauffeur proratisé - amortissement véhicule).
- 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).
- Alertes (2 jours) : email quotidien si marge < 8% sur un trajet, alerte stock gasoil, notification impayé > 45 jours.
- 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 :
- 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.
- 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.
- 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.