Le CSV paraît simple, jusqu’au moment où il casse votre pipeline : accents illisibles dans Excel, décimales françaises qui se transforment en texte, clés de jointure typées différemment, fichiers trop volumineux pour la RAM… Cet article remet de l’ordre et vous donne une boîte à outils “prête prod” pour manipuler des CSV sans mauvaise surprise — du téléchargement jusqu’au stockage colonnaire.
Nous partons des fondamentaux qui font la différence en contexte francophone (UTF-8 vs UTF-8-SIG, séparateur ;, virgule décimale), puis nous enchaînons sur les usages concrets : fusionner des tables avec pandas.merge, traiter des fichiers massifs en streaming (chunksize), accélérer avec Polars (lazy) ou requêter en SQL sur fichiers plats avec DuckDB. La dernière étape convertit proprement vos CSV en Parquet pour gagner en vitesse, en compression et en compatibilité analytique — le tout accompagné d’une hygiène de données minimale (nettoyage, dtypes, validations simples) pour fiabiliser vos flux.
À la clé : des recettes courtes, reproductibles, et des choix assumés (quel encodage, quel séparateur, quel moteur) selon votre cible : Excel FR, ETL, ou entrepôt analytique. Un notebook démo et des jeux d’essai accompagnent le guide pour rejouer chaque cas chez vous, comprendre les pièges typiques et repartir avec une ossature réutilisable dans vos projets.
python-csv-utf-8 — Encodage correct (et Excel qui ouvre sans casser les accents)Quand : vous échangez des CSV entre outils hétérogènes (Unix, Windows, ETL, Excel).
Idée clé : distinguer utf-8 (standard) et utf-8-sig (avec BOM pour Excel Windows).
import csv
# Écriture UTF-8 standard (pour ETL, services, Unix)
with open("data_utf8.csv", "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["nom", "ville"])
w.writerow(["José", "Fès"])
# Écriture "Excel-friendly" (BOM)
with open("data_excel.csv", "w", newline="", encoding="utf-8-sig") as f:
w = csv.writer(f, delimiter=";")
w.writerow(["nom", "ville"])
w.writerow(["José", "Rabat"])
Pièges
newline="" au module csv pour éviter des lignes vides intercalées sous Windows.python-csv-delimiter-point-virgule — CSV “à la française”Quand : exports pour Excel FR (séparateur ;, virgule décimale).
Snippet (module csv)
import csv
with open("export_fr.csv", "w", newline="", encoding="utf-8-sig") as f:
w = csv.writer(f, delimiter=";", quoting=csv.QUOTE_MINIMAL)
w.writerow(["date", "montant"])
w.writerow(["01/10/2025", "1,99"])
Variante Pandas
import pandas as pd
df = pd.DataFrame({"date":["01/10/2025"], "montant":[1.99]})
df.to_csv("export_fr_pandas.csv", sep=";", decimal=",", index=False, encoding="utf-8-sig")
Pièges
, et . dans la même colonne → normaliser avant export.pandas-merge-csv — Fusionner/joindre plusieurs CSV proprementQuand : vous avez des tables séparées (ventes, magasins) à joindre par clé.
import pandas as pd
# Lecture avec dtypes alignés pour éviter les "merge" qui ratent
ventes = pd.read_csv("ventes.csv", dtype={"id_magasin":"string"})
magasins = pd.read_csv("magasins.csv", dtype={"id_magasin":"string"})
# Join (équivalent SQL INNER JOIN)
df = ventes.merge(magasins, on="id_magasin", how="inner")
Concaténer plusieurs fichiers “homogènes”
from pathlib import Path
frames = (pd.read_csv(p) for p in Path("imports").glob("*.csv"))
df = pd.concat(frames, ignore_index=True)
Pièges
int64 vs string) → imposer dtype à la lecture.df["id"].is_unique).pandas-chunksize — Traiter un gros CSV sans exploser la RAMQuand : fichier > RAM (ou vous voulez un pipeline streamé).
Recette “sum par client” en streaming
import pandas as pd
totaux = {}
for chunk in pd.read_csv("big.csv", usecols=["client", "montant"],
dtype={"client":"string", "montant":"float64"},
chunksize=500_000):
s = chunk.groupby("client")["montant"].sum()
for k, v in s.items():
totaux[k] = totaux.get(k, 0.0) + v
out = pd.Series(totaux, name="ca").sort_values(ascending=False).to_frame()
out.to_csv("agregats.csv", index=True, encoding="utf-8")
Pièges
usecols/dtype → vitesse ↑, mémoire ↓.polars-csv — Rapide, parallèle, expressifQuand : performance locale maximale, requêtes expressives.
Idée : utilisez le lazy API pour “pushdown” des filtres/projections.
import polars as pl
# Lecture paresseuse (scan_csv ne charge pas immédiatement)
lf = pl.scan_csv("ventes.csv", separator=";", ignore_errors=True)
res = (lf
.with_columns(pl.col("montant").cast(pl.Float64))
.group_by("client")
.agg(pl.col("montant").sum().alias("ca"))
.sort("ca", descending=True)
.limit(20)
.collect()) # exécution
res.write_csv("top20.csv")
Pièges
cast) si sources hétérogènes.collect() déclenche l’exécution ; sans ça, rien ne s’écrit.duckdb-csv — SQL sur fichiers plats (jointures, agrégats, Parquet)Quand : vous pensez “SQL”, vous voulez joindre plusieurs CSV sans tout charger en RAM.
import duckdb
con = duckdb.connect()
con.execute("""
CREATE OR REPLACE TABLE ventes AS
SELECT * FROM read_csv_auto('ventes_*.csv', IGNORE_ERRORS=TRUE);
""")
# Jointure + agrégat
df = con.execute("""
SELECT m.region, SUM(v.montant) AS ca
FROM ventes v
JOIN read_csv_auto('magasins.csv') m USING(id_magasin)
GROUP BY 1 ORDER BY 2 DESC;
""").df()
# Export direct en Parquet ultra-rapide
con.execute("COPY (SELECT * FROM ventes) TO 'ventes.parquet' (FORMAT PARQUET);")
Pièges
read_csv_auto devine bien, mais expliciter les types sur des datasets “bizarres” reste préférable.requests-telecharger-csv — Télécharger proprement (streaming) puis lireQuand : source HTTP(s) distante.
Recette fiable (stream + taille maîtrisée)
import requests
from pathlib import Path
url = "https://exemple.com/data/ventes.csv"
dest = Path("ventes.csv")
with requests.get(url, stream=True, timeout=30) as r:
r.raise_for_status()
with open(dest, "wb") as f:
for chunk in r.iter_content(chunk_size=1024*64):
if chunk:
f.write(chunk)
# Ensuite : lecture Pandas
import pandas as pd
df = pd.read_csv(dest)
Pièges
.content sur des gros fichiers → RAM inutiles. Préférez stream=True.r.headers.get("Content-Type") si vous doutez du format.convertir-csv-en-parquet — Passage au colonne (stockage/IO/analytics)Quand : vous rejouez souvent des analyses, vous voulez compression + colonnes.
Pandas → Parquet (PyArrow conseillé)
import pandas as pd
df = pd.read_csv("ventes.csv", dtype={"id":"Int64"}) # types explicites
df.to_parquet("ventes.parquet", engine="pyarrow", compression="zstd")
Polars → Parquet
import polars as pl
pl.read_csv("ventes.csv").write_parquet("ventes.parquet", compression="zstd")
DuckDB → Parquet (ultra rapide sur gros fichiers)
import duckdb
duckdb.sql("COPY (SELECT * FROM read_csv_auto('ventes.csv')) TO 'ventes.parquet' (FORMAT PARQUET, COMPRESSION ZSTD)")
Pièges
dtype à l’ingestion.string pour éviter la notation scientifique.nettoyer-donnees-csv-python — Hygiène minimale (avant tout pipeline)Quand : sources hétérogènes, valeurs “sales”, séparateurs locaux.
import pandas as pd
df = pd.read_csv("source.csv", dtype="string") # tout en string → nettoyage sûr
# Espaces, NBSP, normalisation décimale
df = df.apply(lambda s: s.str.replace("\u00A0", "", regex=False).str.strip())
# Colonnes ciblées : nombres FR → nombres Python
for col in ["prix_ht", "montant"]:
if col in df:
df[col] = (df[col]
.str.replace(",", ".", regex=False)
.pipe(pd.to_numeric, errors="coerce"))
# Dates robustes (FR)
if "date_vente" in df:
df["date_vente"] = pd.to_datetime(df["date_vente"], dayfirst=True, errors="coerce")
# Booléens hétérogènes
if "actif" in df:
df["actif"] = df["actif"].str.lower().isin({"1","true","vrai","oui","yes"}).astype("boolean")
# Validation schéma minimal
attendu = {"id", "date_vente", "montant"}
manquantes = attendu - set(df.columns)
if manquantes:
raise ValueError(f"Colonnes manquantes: {sorted(manquantes)}")
Pièges
, et . décimales ; choisir une norme et s’y tenir.to_csv(sep=";", decimal=",", encoding="utf-8-sig").utf-8, sep=",", ISO dates.chunksize (Pandas) ou Polars lazy ou DuckDB.requests en streaming + lecture.utf-8 par défaut, utf-8-sig si Excel FR.; pour Excel FR, , pour pipelines globaux...chunksize ou DuckDB/Polars Lazy.
gold/ventes/date=2025-10/part-*.parquet).Règle simple : DuckDB quand vous “pensez SQL/join massif”, Polars quand vous “pensez perf vectorisée”, Pandas quand vous “pensez business & écosystème”.
import pandas as pd, pandas.api.types as ptypes
def assert_schema(df: pd.DataFrame):
required = {"id","date_vente","montant"}
missing = required - set(df.columns)
if missing: raise ValueError(f"Colonnes manquantes: {sorted(missing)}")
assert ptypes.is_datetime64_any_dtype(df["date_vente"])
assert ptypes.is_numeric_dtype(df["montant"])
montant >= 0, qte > 0.df["id"].is_unique.df["id_magasin"].isin(magasins["id_magasin"]).Pour aller plus loin : pandera (schémas typés), Great Expectations (tests data-doc).
chunksize)usecols=[...] (I/O ↓).Int64 (nullable), string, category (RAM ↓, groupby ↑).read_csv_auto + PROJECTION_PUSHDOWN=TRUE (par défaut) → ne lit que les colonnes requises.dtype_backend="pyarrow" (selon env) → colonnes plus compactes.import os, tempfile, shutil
def safe_to_csv(df, path, **kw):
d = os.path.dirname(path) or "."
with tempfile.NamedTemporaryFile("w", delete=False, dir=d, suffix=".tmp",
encoding=kw.get("encoding","utf-8")) as tmp:
df.to_csv(tmp, **kw)
shutil.move(tmp.name, path) # rename atomique sur même volume
dataset=ventes/date=2025-10/region=CASA/part-0001.parquetChecklist rapide :
.str.replace("\u00A0","").str.strip()"," → "." puis pd.to_numeric(..., errors="coerce")pd.to_datetime(..., dayfirst=True, errors="coerce"){"oui","yes","1"} → True)string, pas en float (évite 1.23e+18)Toujours nettoyer AVANT de convertir les dtypes finaux.
Exposez quelques compteurs par exécution :
NaN par colonne clémontant < 0)Stockez un petit rapport JSON à côté du Parquet (ex. metrics.json).
# cli.py
import typer, duckdb, pandas as pd
from pathlib import Path
app = typer.Typer()
@app.command()
def to_parquet(src: Path, dst: Path):
duckdb.sql("COPY (SELECT * FROM read_csv_auto(?)) TO ? (FORMAT PARQUET, COMPRESSION ZSTD)", [str(src), str(dst)])
@app.command()
def validate(csv_path: Path):
df = pd.read_csv(csv_path, nrows=50_000) # échantillon
# ... assertions schéma/domaines ...
print("OK")
if __name__ == "__main__":
app()
Exemples :
python cli.py to-parquet data/ventes.csv gold/ventes.parquet
python cli.py validate data/ventes.csv
sep, decimal, listes de na_values, mapping booléens, partitions./tmp ; valider les métriques (zéro corruption, colonnes OK).import duckdb, pathlib
src = "bronze/ventes_2025-*.csv"
dst = pathlib.Path("gold/ventes_by_month")
dst.mkdir(parents=True, exist_ok=True)
duckdb.sql(f"""
CREATE OR REPLACE TABLE ventes AS
SELECT * FROM read_csv_auto('{src}', IGNORE_ERRORS=TRUE);
""")
months = duckdb.sql("""
SELECT strftime(date_vente, '%Y-%m') AS ym FROM ventes GROUP BY 1 ORDER BY 1
""").fetchall()
for (ym,) in months:
out = dst / f"ym={ym}" / "part-0001.parquet"
out.parent.mkdir(parents=True, exist_ok=True)
duckdb.sql("""
COPY (
SELECT * FROM ventes WHERE strftime(date_vente, '%Y-%m') = ?
) TO ? (FORMAT PARQUET, COMPRESSION ZSTD)
""", [ym, str(out)])
import polars as pl
res = (pl.scan_csv("bronze/ventes.csv")
.filter(pl.col("montant") > 0)
.group_by(pl.col("id_magasin"))
.agg(pl.sum("montant").alias("ca"))
.sort("ca", descending=True)
.collect())
res.write_parquet("gold/ventes_ca.parquet", compression="zstd")
import csv
(df
.assign(date_vente=lambda d: d["date_vente"].dt.strftime("%d/%m/%Y"))
.to_csv("exports/ventes_excel_fr.csv",
sep=";", decimal=",",
index=False, encoding="utf-8-sig",
quoting=csv.QUOTE_MINIMAL, lineterminator="\n"))
, et . pour les décimales dans la même colonne.read_csv sans dtype/usecols sur gros fichiers (lenteur + RAM).Pandas ou Polars pour 10–50 M de lignes ?
Polars (lazy) aura souvent l’avantage. Mais DuckDB brille quand vous avez beaucoup de joins et aimez le SQL.
Pourquoi Parquet maintenant et pas plus tard ?
Parce que vous gagnez disque, débit, filtrage colonne/partition, et vous facilitez l’analytique (Spark/Trino/Arrow).
utf-8 ou utf-8-sig ?utf-8-sig si la cible est Excel Windows (affichage des accents). Sinon, utf-8.
Vous avez désormais une chaîne cohérente : ingestion robuste (Requests/DuckDB), nettoyage typé (Pandas/Polars), stockage analytique (Parquet partitionné), écriture atomique, validation contrats et observabilité.
C’est cette discipline — plus que tel ou tel paramètre — qui rend vos pipelines CSV prévisibles, rapides, et sûrs.
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.