Python

Python : pandas.to_csv — Exporter propre, fiable et “Excel-friendly”

DataFrame.to_csv devient un contrat d’échange : lisible partout, stable dans le temps, compatible Excel, et sûr en production. Concentrez-vous sur les 7 décisions clés, appliquez une des recettes ci-dessus, et vous obtenez des CSV propres, légers et prédictibles.

1) Les 7 décisions à prendre avant d’exporter

  1. Qui lit votre CSV ?
    – Excel Windows ? → encoding="utf-8-sig" (BOM) et souvent sep=";" en contexte FR.
    – Outil technique (ETL, db, microservice) ? → utf-8, sep=",", pas de BOM.
  2. Index et en-têtes :
    – Données tabulaires “propres” → index=False (souvent).
    – Fichiers appendables → header=not fichier_existe.
  3. Séparateur & culture :
    – France/Excel : sep=";", decimal="," si vous voulez une virgule décimale.
    – Monde/ingénierie : sep=",", decimal=".".
  4. Reproductibilité :
    – Ordre déterministe : df.sort_values([...]) + columns=[...].
    – Types maîtrisés : convertir category/string/Int64 avant export.
  5. Taille & vitesse :
    – Fichiers énormes → chunksize=... + compression ("gzip", "zip").
    – Réseaux/cloud → compression + flux (S3/GCS via storage_options).
  6. Qualité & conformité :
    – NA visibles ? na_rep="NA" (sinon chaîne vide).
    – Champs texte susceptibles de contenir le séparateur → quoting=csv.QUOTE_MINIMAL (défaut) ou QUOTE_ALL.
  7. Robustesse I/O :
    – Append atomique et sûr → écrire dans un fichier temporaire puis rename.
    – Eviter corruption en cas de crash → mode="w" + écriture atomique.

2) Recettes prêtes à l’emploi

A. Export “Excel FR prêt-à-ouvrir”

import csv
df.to_csv(
    "export_excel.csv",
    sep=";",                # point-virgule
    decimal=",",            # virgule décimale
    index=False,
    encoding="utf-8-sig",   # BOM pour Excel Windows
    quoting=csv.QUOTE_MINIMAL
)

B. Export “ingénierie/ETL” (classique, reproductible)

cols = ["id","client","date_vente","ca_ht"]
out = (df[cols]
       .sort_values(["client","date_vente"])
       .assign(date_vente=lambda d: d["date_vente"].dt.strftime("%Y-%m-%d")))

out.to_csv(
    "dataset.csv",
    sep=",",
    index=False,
    encoding="utf-8",
    float_format="%.6f",     # décimales stables
    na_rep=""                # NA vides (souvent attendu par ETL)
)

C. Append contrôlé (écrire la suite sans dupliquer les en-têtes)

from pathlib import Path
import csv

path = Path("log_ventes.csv")
out = df[["ts","client","montant"]].sort_values("ts")

out.to_csv(
    path,
    mode="a",
    header=not path.exists(),   # écrit l’en-tête une seule fois
    index=False,
    encoding="utf-8",
    quoting=csv.QUOTE_MINIMAL,
    line_terminator="\n"
)

D. Fractionner un gros dataset en sharding par mois

from pathlib import Path

Path("out").mkdir(exist_ok=True)
for mois, part in df.groupby(df["date"].dt.to_period("M")):
    part.to_csv(f"out/ventes_{mois}.csv", index=False, encoding="utf-8")

E. Export compressé (taille ↓, réseau ↓)

# Gzip (extension .gz → compression auto-inférée)
df.to_csv("ventes.csv.gz", index=False, encoding="utf-8")

# ZIP avec nom interne contrôlé
df.to_csv(
    "ventes.zip",
    index=False,
    encoding="utf-8",
    compression={"method": "zip", "archive_name": "ventes.csv"}
)

F. Export vers S3 / GCS (fsspec)

df.to_csv(
    "s3://mon-bucket/exports/ventes_2025-10.csv",
    index=False,
    encoding="utf-8",
    storage_options={"anon": False}  # adaptez vos credentials
)

G. Colonnes sensibles (zéros en tête, IDs très longs)

safe = (df.assign(
    code_postal=df["code_postal"].astype("string").str.zfill(5),
    id_str=df["id"].astype("string")  # évite 1.23e+18
))
safe.to_csv("sortie.csv", index=False, encoding="utf-8")

H. Européen “strict” (virgule décimale + quoting partout)

import csv
df.to_csv(
    "eu.csv",
    sep=";",
    decimal=",",
    index=False,
    encoding="utf-8",
    quoting=csv.QUOTE_ALL,  # TOUT entre guillemets
    quotechar='"',
    doublequote=True
)

3) Détails qui font la différence

Format des nombres & arrondis

  • float_format="%.2f" standardise l’affichage à 2 décimales.
  • Pour un rendu exact (finance) → envisagez Decimal en amont, ou formatez des colonnes en chaînes avant export.

Dates stables & lisibles

  • Convertissez vos datetime en chaîne avant to_csv si vous visez un format imposé : df["date"] = df["date"].dt.strftime("%d/%m/%Y") # ou ISO: %Y-%m-%d

NA visibles vs silencieuses

  • na_rep="NA" rend les manquants explicites (utile en audit).
  • Chaîne vide par défaut : propre mais peut masquer des nulls fonctionnels.

Quoting, séparateurs et champs texte

  • Si vos textes contiennent le séparateur ou des sauts de ligne, laissez QUOTE_MINIMAL.
  • Pour forcer la sécurité maximale → QUOTE_ALL.
  • escapechar="\\" peut aider sur des datasets “bizarres”.

Terminateur de ligne & compatibilité

  • line_terminator="\n" (Unix) ; "\r\n" si un système cible l’exige.
  • Évitez les “double lignes” : avec pandas, pas besoin du newline="" propre au module csv de Python.

Encodage & erreurs

  • encoding="utf-8" par défaut, utf-8-sig pour Excel, cp1252 pour environnements très anciens.
  • encoding_errors="replace" pour ne jamais planter (journaliser ensuite les anomalies).

4) Performance & gros volumes

Écriture par blocs

# Ecrit par paquets de 200k lignes (RAM ↘, régularité ↗)
df.to_csv("big.csv", index=False, chunksize=200_000)

Pré-tri et sous-ensemble de colonnes

  • Écrire moins de colonnes (columns=[...]) → disque ↓, réseau ↓.
  • Trier en amont peut accélérer des pipelines aval (diffs, merges).

Compression : le bon compromis

  • gzip : simple, très courant (meilleur ratio que zip en général).
  • zip : pratique pour Windows/Excel (double-clic), mais un peu plus lourd.

5) Patterns anti-bugs (copier-coller)

Écriture atomique (éviter les CSV tronqués)

import os, tempfile, shutil

def safe_to_csv(df, path, **kwargs):
    d = os.path.dirname(path) or "."
    with tempfile.NamedTemporaryFile("w", delete=False, dir=d, suffix=".tmp", encoding=kwargs.get("encoding","utf-8")) as tmp:
        tmp_path = tmp.name
        df.to_csv(tmp, **kwargs)
    shutil.move(tmp_path, path)  # rename atomique sur le même volume

safe_to_csv(df, "stable.csv", index=False, encoding="utf-8")

Append avec en-tête conditionnel

from pathlib import Path

def append_csv(df, path, **kwargs):
    p = Path(path)
    df.to_csv(p, mode="a", header=not p.exists(), **kwargs)

append_csv(df, "events.csv", index=False, encoding="utf-8")

Export “Excel-proof” (FR) factorisé

import csv
def to_csv_excel_fr(df, path):
    df.to_csv(
        path,
        sep=";",
        decimal=",",
        index=False,
        encoding="utf-8-sig",
        quoting=csv.QUOTE_MINIMAL,
        line_terminator="\n"
    )

6) Erreurs fréquentes & parades

  • Zéros en tête disparus (codes, SIRET) : la colonne était numérique → convertir en string + zfill avant export.
  • IDs en notation scientifique : même cause → exporter en string.
  • Colonnes mélangées (types incohérents) : normaliser les dtypes avant to_csv.
  • Excel affiche du “charabia” : utiliser encoding="utf-8-sig" ou cp1252.
  • Séparateur mal interprété par Excel : préférer sep=";" en locale FR.
  • Fichier corrompu après crash : adopter l’écriture atomique (temp + rename).
  • Tailles gigantesques : columns=[...], float_format, chunksize, compression gzip.

7) Check-list opérationnelle (à coller dans vos projets)

  • Public cible identifié (Excel FR vs ETL).
  • Encodage choisi (utf-8 / utf-8-sig / cp1252).
  • Séparateur (; FR / , global) & decimal (, ou .).
  • index=False (souvent) & header conditionnel en append.
  • columns ordonnées + tri pour un export déterministe.
  • float_format / date format stabilisés.
  • na_rep explicite selon besoin.
  • quoting adapté aux textes (minimal vs all).
  • chunksize/compression pour gros volumes.
  • écriture atomique pour la robustesse.

Cas pratique — Export multi-cibles avec pandas.to_csv

Ici un cas pratique complet autour de DataFrame.to_csv : un même DataFrame sert trois publics différents (Excel FR, ETL/warehouse, journal d’événements en append), avec contrôle qualité, robustesse I/O et performance.

Contexte métier

Une PME retail consolide des ventes quotidiennes issues de plusieurs boutiques. Les équipes :

  • Commerce (FR/Excel) veulent un CSV “ouvrable direct” dans Excel Windows.
  • Data/ETL veulent un CSV standardisé, reproductible, prêt pour ingestion.
  • Ops gardent un journal d’événements (append) pour audit.

Contraintes réelles : séparateur FR ;, virgule décimale, NBSP dans les milliers, dates au format français, codes (CP, SIRET) à conserver avec zéros en tête, et volumes pouvant dépasser la RAM certains mois.


1) Ingestion & normalisation (rapide mais stricte)

import pandas as pd
from pathlib import Path

# 1) Lire plusieurs CSV "FR" (point-virgule, virgule décimale, NBSP pour milliers)
def read_csv_fr(path, *, usecols=None, dtypes=None, parse_dates=None):
    return pd.read_csv(
        path,
        sep=";",
        decimal=",",
        thousands="\u00A0",
        usecols=usecols,
        dtype=dtypes,
        parse_dates=parse_dates,
        dayfirst=True,
        na_values=["", "NA", "N/A", "-", "null"]
    )

files = sorted(Path("data/ventes").glob("ventes_2025-0*.csv"))
use = ["date_vente", "magasin", "siret", "code_postal", "article", "qte", "prix_unitaire_ht", "tva"]
dtypes = {"magasin":"string","siret":"string","code_postal":"string","article":"string",
          "qte":"Int64","prix_unitaire_ht":"float64","tva":"float64"}

df = pd.concat((read_csv_fr(f, usecols=use, dtypes=dtypes, parse_dates=["date_vente"]) for f in files),
               ignore_index=True)

# 2) Nettoyages ciblés
df["code_postal"] = df["code_postal"].str.zfill(5)
df["siret"]       = df["siret"].str.zfill(14)

# 3) Calculs métier
df["ca_ht"]  = df["qte"] * df["prix_unitaire_ht"]
df["ca_ttc"] = df["ca_ht"] * (1 + df["tva"])

Contrôles minimaux (qualité) :

# Colonnes attendues / types clés
ATT = {"date_vente":"datetime64[ns]","qte":"Int64","ca_ht":"float64","ca_ttc":"float64"}
assert pd.api.types.is_datetime64_any_dtype(df["date_vente"])
assert pd.api.types.is_integer_dtype(df["qte"])
assert pd.api.types.is_numeric_dtype(df["ca_ht"]) and pd.api.types.is_numeric_dtype(df["ca_ttc"])

# Aucune ligne vide sur les identifiants clés
assert df["siret"].notna().all() and df["code_postal"].notna().all()

2) Export “Excel FR prêt-à-ouvrir”

Objectif : zéro surprise pour un utilisateur Excel Windows FR.
Choix : sep=";", decimal=",", BOM (utf-8-sig), index masqué, quoting minimal.

import csv

cols = ["date_vente","magasin","siret","code_postal","article","qte","prix_unitaire_ht","tva","ca_ht","ca_ttc"]
excel_df = (df[cols]
            .sort_values(["date_vente","magasin","article"])
            .assign(date_vente=lambda d: d["date_vente"].dt.strftime("%d/%m/%Y")))

excel_df.to_csv(
    "exports/ventes_excel_fr.csv",
    sep=";",
    decimal=",",          # Excel FR apprécie la virgule décimale
    index=False,
    encoding="utf-8-sig", # BOM pour Excel Windows
    quoting=csv.QUOTE_MINIMAL,
    lineterminator="\n"
)

Pourquoi ça marche dans Excel :

  • BOM → Excel détecte correctement l’UTF-8.
  • ; + decimal="," → interprétation locale naturelle.
  • Dates déjà formatées en DD/MM/YYYY.

3) Export “ETL/warehouse” reproductible

Objectif : standard international (, comme séparateur, . comme décimale), ISO 8601 pour les dates, tri et ordre de colonnes déterministes.

etl_cols = ["id_ligne","date_vente_iso","magasin","siret","article","qte","ca_ht","ca_ttc"]
etl_df = (df
          .assign(
              id_ligne=lambda d: d.index,  # identifiant déterministe (ou hash si besoin)
              date_vente_iso=lambda d: d["date_vente"].dt.strftime("%Y-%m-%d")
          )[["id_ligne","date_vente_iso","magasin","siret","article","qte","ca_ht","ca_ttc"]]
          .sort_values(["date_vente_iso","magasin","article","id_ligne"])
         )

etl_df.to_csv(
    "exports/ventes_dataset.csv",
    sep=",",
    index=False,
    encoding="utf-8",
    float_format="%.6f",   # nombres stables
    na_rep=""              # NA vides (souvent attendu côté ingestion)
)

Variantes utiles :

  • Compression réseau/stockage : etl_df.to_csv("exports/ventes_dataset.csv.gz", index=False, encoding="utf-8")
  • Zip avec nom interne maîtrisé : etl_df.to_csv( "exports/ventes_dataset.zip", index=False, encoding="utf-8", compression={"method": "zip", "archive_name": "ventes_dataset.csv"} )

4) Journal en append (audit) — en-tête écrit une seule fois

from pathlib import Path
import csv

log_path = Path("exports/journal_evenements.csv")
journal = (df[["date_vente","magasin","article","qte","ca_ht"]]
           .sort_values(["date_vente","magasin"])
           .assign(date_vente=lambda d: d["date_vente"].dt.strftime("%Y-%m-%d")))

journal.to_csv(
    log_path,
    mode="a",
    header=not log_path.exists(),  # en-tête la première fois seulement
    index=False,
    encoding="utf-8",
    quoting=csv.QUOTE_MINIMAL,
    lineterminator="\n"
)

5) Robustesse : écriture atomique (anti-fichiers tronqués)

En prod, on écrit dans un fichier temporaire puis on rename (opération atomique sur le même volume).

import os, tempfile, shutil

def safe_to_csv(df, path, **kwargs):
    d = os.path.dirname(path) or "."
    with tempfile.NamedTemporaryFile("w", delete=False, dir=d, suffix=".tmp",
                                     encoding=kwargs.get("encoding","utf-8")) as tmp:
        tmp_path = tmp.name
        df.to_csv(tmp, **kwargs)
    shutil.move(tmp_path, path)

safe_to_csv(excel_df, "exports/ventes_excel_fr_stable.csv",
            sep=";", decimal=",", index=False, encoding="utf-8-sig", lineterminator="\n")

6) Gros volumes : lecture en morceaux → export streamé

Quand le CSV source dépasse la RAM, on lit en chunks et on écrit au fil de l’eau, en gardant l’en-tête pour le premier bloc uniquement.

from pathlib import Path
import csv

source = "data/ventes/ventes_2025-10.csv"
cible  = Path("exports/ventes_2025-10_stream.csv")
first  = True

for chunk in pd.read_csv(
        source,
        sep=";", decimal=",", thousands="\u00A0",
        dtype=dtypes, parse_dates=["date_vente"], dayfirst=True,
        chunksize=250_000):
    # calculs essentiels sur le chunk
    chunk["ca_ht"]  = chunk["qte"] * chunk["prix_unitaire_ht"]
    chunk["ca_ttc"] = chunk["ca_ht"] * (1 + chunk["tva"])
    # export append
    chunk.to_csv(
        cible,
        sep=";", decimal=",",
        index=False,
        encoding="utf-8-sig",
        quoting=csv.QUOTE_MINIMAL,
        lineterminator="\n",
        header=first,
        mode="a"
    )
    first = False

7) Sharding mensuel (fichiers par mois, prêts pour Excel)

from pathlib import Path
Path("exports/mois").mkdir(parents=True, exist_ok=True)

for per, part in df.groupby(df["date_vente"].dt.to_period("M")):
    out = (part
           .assign(date_vente=lambda d: d["date_vente"].dt.strftime("%d/%m/%Y"))
           .sort_values(["date_vente","magasin","article"]))
    out.to_csv(f"exports/mois/ventes_{per}.csv",
               sep=";", decimal=",", index=False, encoding="utf-8-sig")

8) Tests rapides (sanity checks) après export

# 1) Re-lecture d’un échantillon de l’export Excel FR
probe = pd.read_csv("exports/ventes_excel_fr.csv", sep=";", decimal=",", nrows=100)
assert set(["ca_ht","ca_ttc"]).issubset(probe.columns)

# 2) Vérifier l’unicité d’un identifiant (ETL)
etl_probe = pd.read_csv("exports/ventes_dataset.csv", nrows=200_000)
assert etl_probe["id_ligne"].is_unique

# 3) Pas d’index exporté par inadvertance
with open("exports/ventes_excel_fr.csv", "r", encoding="utf-8-sig") as f:
    header_line = f.readline().rstrip("\n")
    assert not header_line.startswith("Unnamed:")

9) Erreurs fréquentes évitées ici

  • Zéros en tête perdus (CP, SIRET) → colonnes forcées en string + zfill.
  • Excel illisibleutf-8-sig, sep=";", decimal=",".
  • Fichiers tronqués après panne → écriture atomique (tmp → rename).
  • Volumes massifs → lecture par chunksize et append contrôlé.
  • Datasets non déterministes → tri + colonnes fixées + formats de dates et floats stabilisés.

Autres articles

CSV en Python — du téléchargement au...
Le CSV paraît simple, jusqu’au moment où il casse...
En savoir plus
Python : pandas.read_csv — Guide pratique enrichi
Pourquoi read_csv reste incontournableParce qu’il transforme un simple fichier...
En savoir plus
Pratique de l’apprentissage automatique avec scikit-learn et...
Cet article vous guide, pas à pas, de la préparation...
En savoir plus
Python & finance PME — un kit...
Pour une PME, la finance est un pilotage quotidien —...
En savoir plus
Python pour la finance — les fonctions de base
Python pour la finance — les fonctions...
Python est devenu l’outil “couteau suisse” des analystes et contrôleurs...
En savoir plus
Python & Pickle : manipuler les fichiers...
Pickle est la bibliothèque standard de Python pour sérialiser (convertir...
En savoir plus
Programmation Python dans la Pratique : Gestionnaire...
📌 IntroductionLa programmation en Python est idéale pour développer des...
En savoir plus
Guide Pratique de Programmation en Python
Cet article explore un guide complet en programmation python et...
En savoir plus

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *