Fusionner des tables est une opération banale en apparence, mais c’est souvent là que les jeux de données se déforment: doublons inattendus, lignes perdues, clés mal alignées ou colonnes qui se superposent mal. Derrière la requête pandas merge dataframes, on cherche surtout une réponse simple: comment combiner proprement plusieurs DataFrames à partir d’une colonne commune ou d’un index. Je vais aller droit au but avec les bons cas d’usage, des exemples concrets et les pièges qui font perdre du temps.
Les points à retenir avant de fusionner des DataFrames
-
merge()est l’outil principal pour relier deux tables sur une clé commune, comme en SQL. -
how=change complètement le résultat:inner,left,right,outeretcrossne répondent pas au même besoin. -
validate=évite les fusions silencieusement fausses en vérifiant la cardinalité des clés. - Les valeurs nulles peuvent se matcher entre elles dans pandas, ce qui surprend souvent les débutants.
-
join()devient plus lisible quand l’index porte déjà la clé métier.
Comprendre ce que merge() fait vraiment
Je pars d’une idée simple: une fusion n’ajoute pas “juste” des colonnes, elle met en relation deux ensembles de lignes. Avec merge(), pandas cherche les correspondances sur une ou plusieurs clés et reconstruit un DataFrame résultat selon la logique de jointure choisie. Si les colonnes de gauche et de droite contiennent la même clé, l’index d’origine n’est généralement pas le centre de la décision; c’est la clé de jointure qui pilote tout.
Je préfère d’ailleurs expliciter la clé avec on= plutôt que de laisser pandas deviner la colonne commune: le code est plus lisible et les erreurs d’intention apparaissent plus vite. C’est aussi la bonne manière de documenter, sans commentaire, ce que la fusion est censée relier.
C’est pour cela que cette opération ressemble davantage à une jointure de base de données qu’à une concaténation. Pour un besoin de mise en regard entre commandes et clients, ou entre événements et métadonnées, c’est la bonne brique. Si votre but est seulement d’empiler des lignes, ce n’est pas le bon outil, et je reviens sur ce point plus loin.
En pratique, le bon réflexe consiste à identifier d’abord la relation entre les tables: une ligne de gauche doit-elle trouver zéro, une ou plusieurs lignes à droite ? La réponse dicte le type de jointure et évite les surprises de cardinalité. Une fois cette logique claire, le choix du mode de fusion devient beaucoup plus simple.

Choisir le bon type de jointure
Le paramètre how change la philosophie de la fusion. C’est lui qui décide si vous gardez seulement les correspondances, si vous conservez toutes les lignes de gauche, ou si vous voulez une vue complète des deux côtés. Quand une fusion donne un résultat “bizarre”, c’est très souvent parce que le mode n’était pas le bon, pas parce que pandas a mal travaillé.
| Mode | Effet concret | Quand je l’utilise |
|---|---|---|
inner |
Garde uniquement les clés présentes des deux côtés | Rapprochement strict, filtrage, nettoyage de données |
left |
Garde toutes les lignes de gauche et complète avec les correspondances à droite | Enrichir une table principale sans perdre de lignes |
right |
Symétrique de left
|
Cas plus rare, quand la table de droite est la référence |
outer |
Garde tout des deux côtés | Audit, consolidation, comparaison de complétude |
cross |
Produit cartésien de toutes les lignes | Générer toutes les combinaisons possibles, jamais sur de gros volumes sans raison solide |
Sur les versions récentes de pandas, il existe aussi left_anti et right_anti pour conserver uniquement les clés absentes de l’autre côté. Je les garde pour des contrôles ciblés, pas pour la jointure métier standard.
Avec ce cadre, il devient facile de passer du principe à un cas concret sur des clés métiers.
Fusionner sur une colonne commune sans surprise
Le cas le plus fréquent, c’est une clé simple: client_id, order_id, user_id, sku. Supposons que j’aie une table de commandes et une table de clients. Si je veux enrichir les commandes sans perdre de ligne, je pars généralement sur un left join et j’ajoute validate tout de suite. C’est le moyen le plus rapide de repérer une incohérence de données avant qu’elle ne se propage.
import pandas as pd
commandes = pd.DataFrame({
"commande_id": [101, 102, 103, 104],
"client_id": [1, 2, 2, 4],
"montant": [120, 80, 42, 200],
})
clients = pd.DataFrame({
"client_id": [1, 2, 3],
"nom": ["Amina", "Louis", "Camille"],
"ville": ["Paris", "Lyon", "Nantes"],
})
resultat = commandes.merge(
clients,
on="client_id",
how="left",
validate="many_to_one",
indicator=True,
suffixes=("_commande", "_client"),
)
Dans ce cas, la commande liée au client 4 reste bien présente, mais les colonnes issues de clients seront vides pour cette ligne. La colonne _merge aide à voir immédiatement ce qui a matché (both) et ce qui n’a pas trouvé de correspondance (left_only). Si une clé attendue doit être unique à droite, validate="many_to_one" vous protège contre une table client dupliquée.
Je conseille aussi de nommer explicitement les suffixes dès qu’il existe un risque de collision entre colonnes. Attendre l’erreur finale pour découvrir deux colonnes ville_x et ville_y n’apporte rien de bon. Avec un jeu de données réel, ce petit détail améliore beaucoup la lisibilité. Quand la clé n’est plus une seule colonne, la logique reste la même, mais la syntaxe mérite un peu plus d’attention.
Joindre sur plusieurs colonnes ou sur l’index
Beaucoup de cas métier ne tiennent pas sur une seule clé. Un tarif peut dépendre du pays et de la ville, un événement de la date et de l’utilisateur, un catalogue de plusieurs attributs. Dans ce cas, merge() accepte une liste de colonnes, et je l’utilise quand la combinaison des clés est la vraie identité de la ligne.
ventes = pd.DataFrame({
"pays": ["FR", "FR", "BE"],
"ville": ["Paris", "Lyon", "Bruxelles"],
"ca": [1500, 980, 720],
})
geo = pd.DataFrame({
"pays": ["FR", "FR", "BE"],
"ville": ["Paris", "Lyon", "Bruxelles"],
"population": [2148000, 522000, 191000],
})
resultat = ventes.merge(geo, on=["pays", "ville"], how="inner")
Si les colonnes ne portent pas le même nom des deux côtés, left_on et right_on permettent de relier les tables sans renommer les sources à l’avance.
resultat = ventes.merge(
tarifs,
left_on=["pays", "ville"],
right_on=["country", "city"],
how="left",
)
Le point important ici, c’est la cohérence stricte des clés: même ordre, même orthographe, même type. Si la colonne pays contient des codes texte d’un côté et des catégories de l’autre, pandas peut refuser la fusion ou produire un résultat trompeur selon le contexte. Je convertis donc les types avant la jointure quand je sais que les sources n’ont pas été normalisées de la même manière.
Quand l’index est déjà la bonne clé
Si vos DataFrames partagent déjà un index métier, join() est souvent plus propre qu’un merge() classique. C’est particulièrement vrai quand plusieurs tables sont alignées sur le même identifiant et que vous voulez ajouter des colonnes sans réécrire la clé à chaque appel.
base = pd.DataFrame(
{"segment": ["A", "B", "C"]},
index=[1, 2, 3],
)
score = pd.DataFrame(
{"score": [0.8, 0.4, 0.9]},
index=[1, 2, 3],
)
resultat = base.join(score, how="left")
Je retiens une règle simple: si la clé vit dans les colonnes, je commence par merge() ; si la clé vit dans l’index, join() devient souvent plus lisible. Cette distinction évite des reset_index() et set_index() inutiles. Le vrai sujet, ensuite, c’est d’éviter les erreurs silencieuses qui faussent le résultat.
Éviter les pièges qui faussent le résultat
Les fusions ratées sont rarement dues à une seule grosse erreur. Elles viennent plus souvent d’un détail discret: types incompatibles, doublons cachés, valeurs manquantes ou cardinalité mal comprise. C’est là que je passe un peu plus de temps, parce qu’un résultat “plausible” peut être faux sans déclencher d’exception.
Les clés doivent être du même type
1 et "1" ne sont pas la même clé. Si une colonne est stockée en entier et l’autre en chaîne de caractères, la fusion ne matchera pas comme vous l’imaginez. J’aligne donc les types avant l’opération: astype("string"), astype("Int64"), ou une normalisation plus spécifique selon le cas.
Les doublons peuvent multiplier les lignes
Une jointure n’additionne pas seulement des colonnes, elle peut aussi multiplier les lignes. Si une clé apparaît deux fois à gauche et trois fois à droite, vous obtenez six lignes pour cette clé. Ce comportement est correct, mais il faut le vouloir. Pour le contrôler, validate reste mon garde-fou préféré: one_to_one, one_to_many, many_to_one ou many_to_many.
Lire aussi : Shebang Python - Le guide complet pour des scripts fiables
Les valeurs nulles se comportent différemment de SQL
Dans pandas, des valeurs nulles sur les clés peuvent être mises en correspondance entre elles. C’est une différence importante avec beaucoup de jointures SQL et cela surprend encore souvent. Quand je ne veux pas de ce comportement, je filtre explicitement les clés manquantes avant la fusion.
Un autre point simple mais utile: si une partie du résultat vous paraît trop grande, suspectez d’abord une relation plusieurs-à-plusieurs ou un cross involontaire. C’est plus fréquent qu’on ne le pense sur de vrais jeux de données. Une fois ces pièges écartés, le choix de l’outil devient beaucoup plus clair.
Choisir entre merge, join et concat
Je vois souvent ces trois fonctions utilisées comme si elles faisaient la même chose. En réalité, chacune répond à un problème différent. Les confondre rallonge le code et rend les bugs plus difficiles à lire.
| Fonction | Ce qu’elle fait | Quand je la choisis |
|---|---|---|
merge() |
Joint sur des colonnes ou des index avec une logique SQL | Quand j’ai une vraie clé commune et besoin de flexibilité |
join() |
Aligne surtout sur l’index, avec une syntaxe concise | Quand l’index porte déjà l’identifiant métier ou quand j’enchaîne plusieurs tables indexées |
concat() |
Empile des objets le long des lignes ou des colonnes | Quand je n’ai pas de relation de jointure et que je veux simplement assembler des blocs homogènes |
La règle pratique que j’applique est simple: si je dois “retrouver” des lignes d’une table dans une autre, je pense merge() ; si je veux seulement assembler des morceaux compatibles, je pense concat(). Quand les données sont déjà indexées proprement, join() m’évite un peu de bruit syntaxique. Cette décision paraît mineure, mais elle fait gagner du temps sur tous les scripts un peu sérieux.
Le contrôle final que je fais avant d’exécuter une fusion
Avant de lancer une jointure sur des données réelles, je vérifie toujours la même liste. Ce réflexe prend moins d’une minute et évite des heures de débogage quand la table grossit ou quand une source amont change discrètement.
- La clé a-t-elle exactement le même type des deux côtés ?
- La relation attendue est-elle
1:1,1:moum:1? - Ai-je besoin de conserver toutes les lignes de gauche ou seulement les correspondances ?
- Une colonne de contrôle comme
_mergem’aiderait-elle à repérer les absences ? - Les colonnes qui se chevauchent ont-elles des suffixes explicites ?
- L’index est-il déjà la bonne clé, auquel cas
join()serait plus lisible ?
Quand je veux aller vite sans sacrifier la fiabilité, je commence par une fusion simple, j’ajoute validate, puis je regarde le résultat avec un échantillon et une vérification de cardinalité. C’est souvent suffisant pour transformer une jointure fragile en étape de préparation de données robuste, et c’est exactement ce qu’on attend d’un pipeline Python propre.