Python

CSV en Python — du téléchargement au Parquet : 9 cas pratiques

×

Recommandés

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.

1) 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

  • Sans BOM, Excel Windows peut mal détecter l’UTF-8 → accents “cassés”.
  • Toujours passer newline="" au module csv pour éviter des lignes vides intercalées sous Windows.

2) 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

  • Mélanger decimal , et . dans la même colonne → normaliser avant export.

3) pandas-merge-csv — Fusionner/joindre plusieurs CSV proprement

Quand : 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

  • Clés de jointure avec types différents (int64 vs string) → imposer dtype à la lecture.
  • Doublons de clés → valider l’unicité si attendu (df["id"].is_unique).

4) pandas-chunksize — Traiter un gros CSV sans exploser la RAM

Quand : 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

  • Ne pas oublier usecols/dtype → vitesse ↑, mémoire ↓.
  • Sur plusieurs passes, attention aux doublons accumulés (pré-nettoyer).

5) polars-csv — Rapide, parallèle, expressif

Quand : 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

  • Types : soyez explicite (cast) si sources hétérogènes.
  • collect() déclenche l’exécution ; sans ça, rien ne s’écrit.

6) 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.

7) requests-telecharger-csv — Télécharger proprement (streaming) puis lire

Quand : 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.
  • Vérifiez r.headers.get("Content-Type") si vous doutez du format.

8) 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

  • Types “mixtes” → fixer dtype à l’ingestion.
  • IDs longs → lire en string pour éviter la notation scientifique.

9) 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

  • Nettoyer avant les conversions de type.
  • Éviter de mélanger , et . décimales ; choisir une norme et s’y tenir.
  • Toujours contrôler les colonnes attendues et les types essentiels (dates, numériques).

Décider vite : mini-arbre de choix

  • Excel FRto_csv(sep=";", decimal=",", encoding="utf-8-sig").
  • ETL/mondeutf-8, sep=",", ISO dates.
  • Gros fichierschunksize (Pandas) ou Polars lazy ou DuckDB.
  • Jointures lourdes → DuckDB SQL sur CSV ou Polars.
  • Stockage/analytics → convertissez en Parquet dès l’ingestion.
  • Sources HTTPrequests en streaming + lecture.

Check-list finale (copier/colmater)

  • Encodage : utf-8 par défaut, utf-8-sig si Excel FR.
  • Séparateur : ; pour Excel FR, , pour pipelines globaux.
  • Dtypes imposés à la lecture (clés, numériques, dates).
  • Nettoyage “early” : NBSP, trims, décimales FR → ..
  • Gros volumes : chunksize ou DuckDB/Polars Lazy.
  • Export stable : ordre des colonnes + formats (dates/float).
  • Conversion Parquet pour accélérer les analyses récurrentes.
  • Tests rapides (échantillon relu, colonnes attendues, unicités clés).

CSV → Parquet en production : qualité, perf & fiabilité

1) Architecture de flux (choisir l’outil au bon endroit)

  • Ingestion (Bronze) : CSV bruts, sans transformation.
    • DuckDB pour pré-filtrer/joiner des CSV massifs sans charger en RAM.
    • Requests (stream) pour télécharger depuis HTTP/S3.
  • Nettoyage/typage (Silver) :
    • Pandas pour les transformations business (lisibilité, écosystème).
    • Polars (lazy) si volumétrie et CPU sont critiques (projection/predicate pushdown).
  • Stockage analytique (Gold) :
    • Convertir tôt en Parquet (colonnaire, compressé).
    • Partitionner par date / source / région pour accélérer les lectures aval (ex. 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”.


2) Contrats de données & validation (empêcher le sale de passer)

Schéma minimal “maison”

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"])

Points durs à ajouter

  • Domaines : montant >= 0, qte > 0.
  • Clés : df["id"].is_unique.
  • Référentiels : df["id_magasin"].isin(magasins["id_magasin"]).

Pour aller plus loin : pandera (schémas typés), Great Expectations (tests data-doc).


3) Performance avancée (au-delà de chunksize)

  • Colonnes utiles : usecols=[...] (I/O ↓).
  • Dtypes explicites : Int64 (nullable), string, category (RAM ↓, groupby ↑).
  • CSV → Polars (lazy) : le moteur pousse les filtres au lecteur de fichiers.
  • CSV → DuckDB : read_csv_auto + PROJECTION_PUSHDOWN=TRUE (par défaut) → ne lit que les colonnes requises.
  • Compression :
    • Lire des gzip peut être CPU-bound. Si réseau rapide/local, préférez CSV non compressés en ingestion, puis compressez en sortie Parquet (ZSTD).
  • PyArrow backend (Pandas) : dtype_backend="pyarrow" (selon env) → colonnes plus compactes.

4) Écritures sûres & partitionnement (éviter les fichiers corrompus)

Écriture atomique (pattern)

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

Partitionner vos sorties

  • Nommer : dataset=ventes/date=2025-10/region=CASA/part-0001.parquet
  • Bénéfices : lecture sélective (prune), parallélisation map-reduce friendly, re-process partiel.

5) Nettoyage robuste (hygiène “early”)

Checklist rapide :

  • NBSP/espaces.str.replace("\u00A0","").str.strip()
  • Décimales FR"," → "." puis pd.to_numeric(..., errors="coerce")
  • Datespd.to_datetime(..., dayfirst=True, errors="coerce")
  • Booléens → mapping explicite ({"oui","yes","1"} → True)
  • Codes/IDs → lire en string, pas en float (évite 1.23e+18)

Toujours nettoyer AVANT de convertir les dtypes finaux.


6) Observabilité & traçabilité (savoir ce qui s’est passé)

Exposez quelques compteurs par exécution :

  • Lignes lues / écrites
  • % de NaN par colonne clé
  • Nombre de lignes coercées (dates/nombres)
  • Valeurs hors domaine (ex. montant < 0)
  • Temps par étape (download, read_csv, nettoyages, export parquet)
  • Taux de pruning (DuckDB/Polars) si dispo

Stockez un petit rapport JSON à côté du Parquet (ex. metrics.json).


7) Sécurité & conformité (PII / RGPD)

  • Minimiser : n’ingérer que les colonnes nécessaires.
  • Hashing/anonymisation des IDs sensibles pour les environnements non-prod.
  • Journal des accès et des exports (qui, quand, quoi).
  • Chiffrement au repos si cloud (S3 SSE/KMS) et en transit (HTTPS).
  • Purge planifiée des CSV bruts après conversion en Parquet si non requis.

8) Packaging & exécution (du notebook au job)

CLI minimal avec Typer (ou Click)

# 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

Config externalisée

  • YAML / ENV pour sep, decimal, listes de na_values, mapping booléens, partitions.
  • Avantage : vous changez d’environnement sans toucher le code.

9) Tests & CI (la garde rapprochée)

  • Unitaires : fonctions de nettoyage (ex. “1 234,50” → 1234.50).
  • Contrats : un échantillon de CSV “sale” + snapshot du Parquet attendu.
  • Hypothesis (property-based) : générer des valeurs aléatoires “casse cou”.
  • CI : lancer le pipeline sur des mini-fixtures (1000 lignes) ; exporter en /tmp ; valider les métriques (zéro corruption, colonnes OK).

10) Exemples “patterns” prêts à insérer

A. “ETL léger” DuckDB → Parquet partitionné par mois

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)])

B. “Silver” Polars (lazy) : filtre + agrégat haute perf

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")

C. “Excel FR export” stable (Pandas)

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"))

11) Anti-patterns (à éviter absolument)

  • Lire des IDs en float (zéros perdus / notation scientifique).
  • Mélanger , et . pour les décimales dans la même colonne.
  • read_csv sans dtype/usecols sur gros fichiers (lenteur + RAM).
  • Exporter des CSV sans écriture atomique (fichiers tronqués).
  • Garder éternellement des CSV bruts sensibles en clair.
  • Dépendre d’un notebook non versionné pour un job de prod.

Mini-FAQ avancée

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.

Recommandés

Python : pandas.to_csv — Exporter propre, fiable...
DataFrame.to_csv devient un contrat d’échange :...
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 !!