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.
encoding="utf-8-sig" (BOM) et souvent sep=";" en contexte FR.utf-8, sep=",", pas de BOM.index=False (souvent).header=not fichier_existe.sep=";", decimal="," si vous voulez une virgule décimale.sep=",", decimal=".".df.sort_values([...]) + columns=[...].category/string/Int64 avant export.chunksize=... + compression ("gzip", "zip").storage_options).na_rep="NA" (sinon chaîne vide).quoting=csv.QUOTE_MINIMAL (défaut) ou QUOTE_ALL.rename.mode="w" + écriture atomique.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
)
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)
)
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"
)
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")
# 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"}
)
df.to_csv(
"s3://mon-bucket/exports/ventes_2025-10.csv",
index=False,
encoding="utf-8",
storage_options={"anon": False} # adaptez vos credentials
)
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")
import csv
df.to_csv(
"eu.csv",
sep=";",
decimal=",",
index=False,
encoding="utf-8",
quoting=csv.QUOTE_ALL, # TOUT entre guillemets
quotechar='"',
doublequote=True
)
float_format="%.2f" standardise l’affichage à 2 décimales.Decimal en amont, ou formatez des colonnes en chaînes avant export.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-%dna_rep="NA" rend les manquants explicites (utile en audit).QUOTE_MINIMAL.QUOTE_ALL.escapechar="\\" peut aider sur des datasets “bizarres”.line_terminator="\n" (Unix) ; "\r\n" si un système cible l’exige.newline="" propre au module csv de Python.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).# Ecrit par paquets de 200k lignes (RAM ↘, régularité ↗)
df.to_csv("big.csv", index=False, chunksize=200_000)
columns=[...]) → disque ↓, réseau ↓.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")
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")
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"
)
string + zfill avant export.string.to_csv.encoding="utf-8-sig" ou cp1252.sep=";" en locale FR.columns=[...], float_format, chunksize, compression gzip.utf-8 / utf-8-sig / cp1252).; FR / , global) & decimal (, ou .).pandas.to_csvIci 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.
Une PME retail consolide des ventes quotidiennes issues de plusieurs boutiques. Les équipes :
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.
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()
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 :
; + decimal="," → interprétation locale naturelle.DD/MM/YYYY.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 :
etl_df.to_csv("exports/ventes_dataset.csv.gz", index=False, encoding="utf-8")etl_df.to_csv( "exports/ventes_dataset.zip", index=False, encoding="utf-8", compression={"method": "zip", "archive_name": "ventes_dataset.csv"} )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"
)
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")
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
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")
# 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:")
string + zfill.utf-8-sig, sep=";", decimal=",".chunksize et append contrôlé.
Deux outils concrets pour piloter la qualité sans alourdir vos équipes Un système qualité n’avance…
Un chantier se gagne souvent avant même l’arrivée des équipes. Quand tout est clair dès…
Le mariage a du sens quand il repose sur une décision libre, mûrie et partagée.…
Une étude de cas réussie commence par une structure sûre. Ce modèle Word vous guide…
Les soft skills se repèrent vite sur une fiche, mais elles ne pèsent vraiment que…
Outil de comparaison et repérage des offres étudiantes Choisir des verres progressifs ressemble rarement à…
This website uses cookies.