Python

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

×

Recommandés

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.

Recommandés

CSV en Python — du téléchargement au...
Le CSV paraît simple, jusqu’au...
En savoir plus
Python : pandas.read_csv — Guide pratique enrichi
Pourquoi read_csv reste incontournableParce qu’il...
En savoir plus
Pratique de l’apprentissage automatique avec scikit-learn et...
Cet article vous guide, pas à...
En savoir plus
Python & finance PME — un kit...
Pour une PME, la finance est...
En savoir plus
Python pour la finance — les fonctions de base
Python pour la finance — les fonctions...
Python est devenu l’outil “couteau suisse”...
En savoir plus
Python & Pickle : manipuler les fichiers...
Pickle est la bibliothèque standard de...
En savoir plus

Laisser un commentaire

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

error: Content is protected !!