Introduction : L'Écosystème Python pour les Données en 2026
Si vous travaillez avec des données en Python en 2026, vous avez probablement remarqué que le paysage a pas mal changé. Pandas a longtemps régné en maître absolu sur la manipulation de données tabulaires, mais aujourd'hui, les professionnels adoptent des workflows multi-outils qui combinent les forces de plusieurs bibliothèques.
Et honnêtement, c'est logique.
Les volumes de données explosent, les contraintes de temps de traitement deviennent critiques en production, et plutôt que de chercher LA solution unique universelle, les data engineers et data scientists modernes orchestrent intelligemment différents outils pour créer des pipelines optimaux. Trois acteurs majeurs dominent ce nouvel écosystème hybride : Pandas avec sa maturité et son intégration dans l'écosystème scientifique Python, Polars qui s'impose grâce à son implémentation en Rust, et DuckDB qui révolutionne l'analyse avec son moteur SQL analytique ultra-rapide.
Dans ce guide, on va explorer ces trois outils en profondeur et apprendre à les combiner efficacement. Allez, on plonge dedans.
Présentation des Trois Bibliothèques
Pandas : La Référence Mature
Pandas reste la bibliothèque la plus utilisée dans l'écosystème Python pour la manipulation de données. Créée en 2008 par Wes McKinney, elle offre une API riche et intuitive avec ses structures emblématiques : Series et DataFrame. Avec plus d'une décennie de développement, elle bénéficie d'une documentation exhaustive, d'une communauté massive et d'une intégration profonde avec NumPy, SciPy, scikit-learn, Matplotlib — bref, tout l'écosystème scientifique Python.
En 2026, Pandas 3.0 a introduit des améliorations significatives, notamment le backend PyArrow par défaut et la sémantique Copy-on-Write. Malgré l'émergence de concurrents plus rapides, Pandas reste le choix privilégié pour l'exploration interactive, l'intégration avec Jupyter et les projets nécessitant une compatibilité maximale. C'est un peu le couteau suisse qu'on connaît tous.
Polars : La Performance par le Rust
Polars, c'est la bibliothèque qui fait tourner les têtes depuis quelques années. Développée en Rust avec des bindings Python, lancée en 2020 par Ritchie Vink, elle a rapidement gagné en popularité grâce à des performances franchement impressionnantes. On parle de vitesses 10 à 30 fois supérieures à Pandas sur de nombreuses opérations, grâce au parallélisme moderne, à l'évaluation paresseuse et à des optimisations de requêtes sophistiquées.
L'architecture repose sur deux concepts fondamentaux : le DataFrame pour l'exécution immédiate et le LazyFrame pour l'optimisation des requêtes. Le moteur analyse l'ensemble de la chaîne d'opérations avant l'exécution, appliquant des optimisations comme le predicate pushdown et le projection pushdown. Son API basée sur les expressions offre une syntaxe élégante et composable — une fois qu'on y a goûté, difficile de revenir en arrière.
DuckDB : Le Moteur SQL Analytique
DuckDB est souvent décrit comme le « SQLite de l'analytique », et la comparaison est plutôt pertinente. Contrairement aux bases de données traditionnelles, DuckDB s'exécute directement dans le même processus que votre application Python, sans serveur distinct. Il excelle dans les requêtes analytiques OLAP et peut traiter des données directement depuis des fichiers CSV, Parquet ou JSON sans importation préalable.
Ce qui est vraiment remarquable, c'est son intégration avec Python : DuckDB peut interroger directement des DataFrames Pandas ou Polars en mémoire comme s'il s'agissait de tables SQL, avec une conversion zero-copy dans de nombreux cas. En 2026, il est devenu l'outil de prédilection pour les requêtes ad-hoc, les agrégations complexes et l'interrogation de fichiers volumineux.
Quand Utiliser Chaque Outil
Scénarios d'Utilisation de Pandas
Privilégiez Pandas dans les situations suivantes : exploration interactive dans des notebooks Jupyter, projets nécessitant une compatibilité avec l'écosystème scientifique existant, manipulation de séries temporelles avancée, intégration avec Matplotlib ou Seaborn, et quand vous travaillez avec des équipes déjà familiarisées avec son API.
C'est aussi un excellent choix pour les datasets de taille petite à moyenne (quelques millions de lignes) où les performances ne sont pas critiques, et pour le prototypage rapide. La documentation est tellement fournie qu'on trouve presque toujours un exemple pour ce qu'on cherche.
Scénarios d'Utilisation de Polars
Optez pour Polars quand les performances deviennent critiques. On parle de datasets volumineux (dizaines ou centaines de millions de lignes), de pipelines de production nécessitant une faible latence, et d'opérations intensives en calcul comme les agrégations complexes, les jointures multiples ou les transformations de colonnes.
Polars brille particulièrement quand on exploite l'évaluation paresseuse pour optimiser automatiquement les requêtes. C'est aussi le choix idéal pour les nouveaux projets sans contrainte de compatibilité legacy, et quand on veut une API moderne et expressive.
Scénarios d'Utilisation de DuckDB
DuckDB est parfait pour les requêtes SQL complexes sur des données tabulaires, l'analyse ad-hoc avec des agrégations sophistiquées, et l'interrogation directe de fichiers Parquet, CSV ou JSON sans chargement préalable en mémoire.
Utilisez-le quand vous préférez la syntaxe SQL pour exprimer vos transformations (et franchement, pour certaines opérations analytiques, le SQL est imbattable), quand vous devez interroger des données trop volumineuses pour la RAM, ou quand vous voulez combiner des données de plusieurs sources dans une seule requête.
Installation et Configuration
L'installation est simple et directe via pip. Pour un environnement complet en 2026, on recommande aussi d'installer PyArrow pour améliorer les performances et l'interopérabilité.
# Installation des bibliothèques principales
pip install pandas polars duckdb
# Installation de PyArrow pour de meilleures performances
pip install pyarrow
# Vérification des installations
import pandas as pd
import polars as pl
import duckdb
print(f"Pandas version: {pd.__version__}")
print(f"Polars version: {pl.__version__}")
print(f"DuckDB version: {duckdb.__version__}")
Pour des performances optimales avec Pandas 3.0, configurez le backend PyArrow par défaut :
import pandas as pd
# Configuration globale pour utiliser PyArrow
pd.options.mode.dtype_backend = 'pyarrow'
# Le Copy-on-Write est activé par défaut dans Pandas 3.0
pd.options.mode.copy_on_write = True
Polars en Profondeur
Évaluation Paresseuse et LazyFrames
L'évaluation paresseuse est probablement le concept le plus puissant de Polars. Au lieu d'exécuter chaque opération immédiatement, Polars construit un plan d'exécution qu'il optimise avant de matérialiser les résultats. C'est un peu comme si un compilateur analysait tout votre pipeline pour trouver le chemin le plus efficace.
import polars as pl
# Lecture paresseuse d'un fichier CSV
lazy_df = pl.scan_csv("large_dataset.csv")
# Chaînage d'opérations sans exécution immédiate
result = (
lazy_df
.filter(pl.col("age") > 18)
.select([
pl.col("name"),
pl.col("email"),
pl.col("purchase_amount")
])
.group_by("name")
.agg([
pl.col("purchase_amount").sum().alias("total_spent"),
pl.col("purchase_amount").count().alias("num_purchases")
])
.filter(pl.col("total_spent") > 1000)
.sort("total_spent", descending=True)
)
# Le plan d'exécution est optimisé automatiquement
print(result.explain())
# Exécution et matérialisation des résultats
final_df = result.collect()
print(final_df)
Dans cet exemple, Polars applique plusieurs optimisations automatiques : le predicate pushdown déplace les filtres le plus tôt possible pour réduire le volume de données traité, et le projection pushdown ne lit que les colonnes nécessaires depuis le fichier CSV. Le résultat ? Une utilisation mémoire drastiquement réduite et un temps d'exécution bien meilleur.
API des Expressions
L'API d'expressions de Polars est vraiment un plaisir à utiliser. Hautement composable, elle permet de construire des transformations complexes de manière élégante. Les expressions se combinent avec des opérateurs et peuvent être réutilisées dans différents contextes.
import polars as pl
# Création d'un DataFrame d'exemple
df = pl.DataFrame({
"product": ["Laptop", "Mouse", "Keyboard", "Monitor", "Laptop"],
"price": [1200, 25, 75, 350, 1100],
"quantity": [1, 3, 2, 1, 1],
"category": ["Electronics", "Accessories", "Accessories",
"Electronics", "Electronics"]
})
# Utilisation d'expressions pour créer de nouvelles colonnes
result = df.select([
pl.col("product"),
pl.col("price"),
pl.col("quantity"),
(pl.col("price") * pl.col("quantity")).alias("total"),
pl.col("price").rank(method="ordinal").alias("price_rank"),
pl.when(pl.col("price") > 500)
.then(pl.lit("Premium"))
.otherwise(pl.lit("Standard"))
.alias("tier")
])
print(result)
# Expressions dans les agrégations
aggregated = df.group_by("category").agg([
pl.col("price").mean().alias("avg_price"),
pl.col("price").min().alias("min_price"),
pl.col("price").max().alias("max_price"),
pl.col("quantity").sum().alias("total_quantity"),
pl.col("product").n_unique().alias("unique_products")
])
print(aggregated)
Contextes d'Exécution
Polars offre plusieurs contextes pour appliquer des expressions : select pour créer ou transformer des colonnes, with_columns pour en ajouter tout en conservant les existantes, filter pour filtrer les lignes, et group_by suivi de agg pour les agrégations. Chacun a sa place selon ce qu'on veut faire.
import polars as pl
from datetime import date
# DataFrame avec des données temporelles
df = pl.DataFrame({
"date": [date(2026, 1, i) for i in range(1, 11)],
"sales": [100, 150, 120, 200, 180, 220, 190, 250, 210, 230],
"costs": [60, 90, 70, 120, 110, 130, 115, 150, 125, 140]
})
# Utilisation de with_columns pour ajouter des calculs
enriched = df.with_columns([
(pl.col("sales") - pl.col("costs")).alias("profit"),
(pl.col("sales") / pl.col("costs")).alias("margin_ratio"),
pl.col("sales").rolling_mean(window_size=3).alias("sales_ma3")
])
print(enriched)
# Expressions conditionnelles complexes
classified = df.with_columns([
pl.when(pl.col("sales") > 200)
.then(pl.lit("Élevé"))
.when(pl.col("sales") > 150)
.then(pl.lit("Moyen"))
.otherwise(pl.lit("Faible"))
.alias("performance")
])
print(classified)
Pourquoi Polars est si Rapide
Les performances de Polars ne viennent pas de nulle part. Plusieurs facteurs se combinent : les types de données Arrow pour une représentation mémoire efficace, la parallélisation automatique sur tous les coeurs CPU, l'implémentation en Rust qui élimine le fameux Global Interpreter Lock (GIL) de Python, et des algorithmes optimisés pour les opérations courantes. En pratique, la différence est vraiment palpable dès qu'on dépasse quelques centaines de milliers de lignes.
import polars as pl
import time
# Création d'un large dataset pour le benchmark
n_rows = 10_000_000
df = pl.DataFrame({
"id": range(n_rows),
"value": [i * 2.5 for i in range(n_rows)],
"category": [f"cat_{i % 100}" for i in range(n_rows)]
})
# Opération d'agrégation complexe
start = time.time()
result = df.group_by("category").agg([
pl.col("value").mean().alias("mean_value"),
pl.col("value").std().alias("std_value"),
pl.col("value").quantile(0.95).alias("p95_value"),
pl.col("id").count().alias("count")
])
elapsed = time.time() - start
print(f"Temps d'exécution Polars: {elapsed:.2f}s")
print(f"Nombre de lignes traitées: {n_rows:,}")
print(f"Vitesse: {n_rows/elapsed:,.0f} lignes/seconde")
DuckDB en Profondeur
SQL sur les DataFrames
La capacité de DuckDB à interroger directement des DataFrames Pandas ou Polars avec du SQL standard est franchement bluffante. Pour les analystes à l'aise avec SQL qui veulent rester dans l'écosystème Python, c'est un vrai game-changer.
import duckdb
import pandas as pd
# Création de DataFrames Pandas
df_customers = pd.DataFrame({
"customer_id": [1, 2, 3, 4, 5],
"name": ["Alice", "Bob", "Charlie", "Diana", "Eve"],
"country": ["France", "USA", "France", "UK", "USA"]
})
df_orders = pd.DataFrame({
"order_id": [101, 102, 103, 104, 105, 106],
"customer_id": [1, 1, 2, 3, 3, 4],
"amount": [150, 200, 300, 120, 180, 250]
})
# Interrogation directe avec SQL
result = duckdb.sql("""
SELECT
c.name,
c.country,
COUNT(o.order_id) AS num_orders,
SUM(o.amount) AS total_spent,
AVG(o.amount) AS avg_order_value
FROM df_customers c
LEFT JOIN df_orders o ON c.customer_id = o.customer_id
GROUP BY c.name, c.country
ORDER BY total_spent DESC
""")
# Conversion en DataFrame Pandas
result_df = result.df()
print(result_df)
# Conversion en DataFrame Polars
result_pl = result.pl()
print(result_pl)
Intégration Zero-Copy
L'une des vraies forces de DuckDB, c'est sa capacité à accéder aux données des DataFrames sans copie mémoire, notamment quand les DataFrames utilisent le format Arrow en interne. Ça permet des requêtes ultra-rapides même sur de gros volumes.
import duckdb
import polars as pl
# Création d'un DataFrame Polars (utilise Arrow nativement)
large_df = pl.DataFrame({
"id": range(5_000_000),
"value": [i * 1.5 for i in range(5_000_000)],
"category": [f"category_{i % 50}" for i in range(5_000_000)]
})
# Connexion DuckDB réutilisable
con = duckdb.connect()
# Enregistrement du DataFrame comme une vue
con.register("large_table", large_df)
# Requêtes SQL complexes sans copie mémoire
result = con.execute("""
SELECT
category,
COUNT(*) AS count,
AVG(value) AS avg_value,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY value) AS median_value,
STDDEV(value) AS std_value
FROM large_table
WHERE value > 1000
GROUP BY category
HAVING count > 10000
ORDER BY avg_value DESC
LIMIT 10
""").pl()
print(result)
Interrogation Directe de Fichiers
DuckDB peut interroger directement des fichiers CSV, Parquet, JSON sans les charger entièrement en mémoire. Pour l'analyse de fichiers qui dépassent la RAM disponible, c'est tout simplement révolutionnaire.
import duckdb
# Interrogation directe d'un fichier Parquet
result = duckdb.sql("""
SELECT
product_category,
DATE_TRUNC('month', order_date) AS month,
SUM(order_amount) AS monthly_revenue,
COUNT(DISTINCT customer_id) AS unique_customers
FROM 'sales_data.parquet'
WHERE order_date >= '2025-01-01'
GROUP BY product_category, month
ORDER BY month, monthly_revenue DESC
""")
print(result.df())
# Interrogation de plusieurs fichiers avec des patterns glob
result_multi = duckdb.sql("""
SELECT
region,
COUNT(*) AS total_transactions,
SUM(amount) AS total_revenue
FROM 'data/sales_*.csv'
GROUP BY region
""")
print(result_multi.df())
# Conversion de format : CSV vers Parquet optimisé
duckdb.sql("""
COPY (
SELECT *
FROM 'large_input.csv'
WHERE status = 'active'
) TO 'filtered_output.parquet' (FORMAT PARQUET, COMPRESSION ZSTD)
""")
Fonctions Analytiques Avancées
DuckDB supporte des fonctions analytiques SQL sophistiquées comme les window functions et les fonctions statistiques avancées. Pour certaines opérations (comme les moyennes glissantes ou les classements), c'est nettement plus lisible qu'avec Pandas ou Polars.
import duckdb
import pandas as pd
from datetime import datetime, timedelta
# Données de séries temporelles
dates = [datetime(2026, 1, 1) + timedelta(days=i) for i in range(30)]
df_timeseries = pd.DataFrame({
"date": dates,
"revenue": [1000 + i * 50 + (i % 7) * 100 for i in range(30)],
"users": [500 + i * 10 for i in range(30)]
})
# Utilisation de window functions
result = duckdb.sql("""
SELECT
date,
revenue,
users,
AVG(revenue) OVER (
ORDER BY date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS revenue_7day_avg,
revenue - LAG(revenue, 1) OVER (ORDER BY date) AS revenue_daily_change,
ROW_NUMBER() OVER (ORDER BY revenue DESC) AS revenue_rank,
PERCENT_RANK() OVER (ORDER BY revenue) AS revenue_percentile
FROM df_timeseries
ORDER BY date
""").df()
print(result.head(10))
Pandas 3.0 en 2026
Backend PyArrow
Le passage au backend PyArrow par défaut dans Pandas 3.0, c'est probablement la mise à jour la plus impactante de ces dernières années pour la bibliothèque. Fini les types NumPy traditionnels par défaut : on gagne en performance, en gestion mémoire et en interopérabilité avec Polars et DuckDB.
import pandas as pd
# Création d'un DataFrame avec le backend PyArrow
df = pd.DataFrame({
"integers": pd.array([1, 2, 3, 4, 5], dtype="int64[pyarrow]"),
"strings": pd.array(["a", "b", "c", "d", "e"], dtype="string[pyarrow]"),
"floats": pd.array([1.1, 2.2, 3.3, 4.4, 5.5], dtype="float64[pyarrow]"),
"dates": pd.date_range("2026-01-01", periods=5)
})
# Vérification des types
print(df.dtypes)
# Lecture CSV avec le moteur PyArrow
df_csv = pd.read_csv(
"data.csv",
dtype_backend="pyarrow",
engine="pyarrow"
)
# Conversion d'un DataFrame existant vers les types PyArrow
df_legacy = pd.DataFrame({
"col1": [1, 2, 3],
"col2": ["x", "y", "z"]
})
df_arrow = df_legacy.astype({
"col1": "int64[pyarrow]",
"col2": "string[pyarrow]"
})
print(df_arrow.dtypes)
Sémantique Copy-on-Write
Si vous avez déjà eu des sueurs froides avec le fameux SettingWithCopyWarning de Pandas, vous allez adorer le Copy-on-Write. La sémantique CoW élimine l'un des pièges les plus frustrants de Pandas : les copies implicites et les vues ambiguës. Les opérations sont plus prévisibles, et les copies ne sont créées que lorsque c'est vraiment nécessaire.
import pandas as pd
# Copy-on-Write activé par défaut dans Pandas 3.0
pd.options.mode.copy_on_write = True
# Création d'un DataFrame
df = pd.DataFrame({
"A": [1, 2, 3, 4],
"B": [10, 20, 30, 40],
"C": [100, 200, 300, 400]
})
# Avec CoW, cette opération ne crée pas de copie immédiate
df_subset = df[["A", "B"]]
# La copie n'est créée que lors de la modification
df_subset["A"] = [5, 6, 7, 8]
# Le DataFrame original n'est PAS affecté
print("DataFrame original:")
print(df)
print("\nDataFrame modifié:")
print(df_subset)
Construire un Pipeline Hybride
Bon, maintenant on arrive au coeur du sujet : combiner les trois outils dans un pipeline concret. C'est là que la magie opère vraiment. L'idée est simple — utiliser DuckDB pour l'ingestion et les requêtes SQL, Polars pour les transformations haute performance, et Pandas pour l'analyse finale et la visualisation.
Scénario : Analyse de Données E-commerce
Imaginons qu'on doive analyser des données de ventes e-commerce réparties sur plusieurs fichiers Parquet volumineux, effectuer des transformations complexes et produire des statistiques pour un tableau de bord. Voici comment on peut structurer ça :
import duckdb
import polars as pl
import pandas as pd
# === Étape 1 : Ingestion avec DuckDB ===
# DuckDB lit efficacement plusieurs fichiers Parquet avec préfiltrage SQL
print("Étape 1 : Chargement des données avec DuckDB...")
query = """
SELECT
order_id, customer_id, product_id,
product_category, order_date, order_amount,
quantity, discount_percent, shipping_country
FROM 'data/sales_2026_*.parquet'
WHERE order_date >= '2026-01-01'
AND order_date < '2026-02-01'
AND order_amount > 0
"""
# Chargement directement dans Polars
df_polars = duckdb.sql(query).pl()
print(f"Données chargées : {len(df_polars):,} lignes")
# === Étape 2 : Transformations avec Polars ===
print("\nÉtape 2 : Transformations avec Polars...")
df_transformed = (
df_polars.lazy()
.with_columns([
(pl.col("order_amount") * (1 - pl.col("discount_percent") / 100))
.alias("net_amount")
])
.with_columns([
pl.col("order_date").dt.year().alias("year"),
pl.col("order_date").dt.month().alias("month"),
pl.col("order_date").dt.weekday().alias("weekday")
])
.with_columns([
pl.when(pl.col("net_amount") > 500)
.then(pl.lit("Haute Valeur"))
.when(pl.col("net_amount") > 100)
.then(pl.lit("Valeur Moyenne"))
.otherwise(pl.lit("Faible Valeur"))
.alias("segment_client")
])
.with_columns([
(pl.col("net_amount") / pl.col("quantity")).alias("prix_unitaire")
])
.collect()
)
print(f"Transformations appliquées : {len(df_transformed):,} lignes")
# === Étape 3 : Agrégations complexes avec DuckDB ===
print("\nÉtape 3 : Agrégations avec DuckDB...")
con = duckdb.connect()
con.register("sales_transformed", df_transformed)
aggregated_query = """
WITH daily_sales AS (
SELECT
order_date,
product_category,
shipping_country,
COUNT(DISTINCT customer_id) AS unique_customers,
COUNT(*) AS num_orders,
SUM(net_amount) AS daily_revenue,
AVG(net_amount) AS avg_order_value
FROM sales_transformed
GROUP BY order_date, product_category, shipping_country
),
ranked AS (
SELECT
*,
ROW_NUMBER() OVER (
PARTITION BY order_date
ORDER BY daily_revenue DESC
) AS category_rank,
SUM(daily_revenue) OVER (
PARTITION BY product_category
ORDER BY order_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS revenue_7j_glissant
FROM daily_sales
)
SELECT * FROM ranked
ORDER BY order_date, category_rank
"""
df_aggregated = con.execute(aggregated_query).pl()
# === Étape 4 : Analyse finale avec Pandas ===
print("\nÉtape 4 : Analyse finale avec Pandas...")
df_pandas = df_aggregated.to_pandas()
category_summary = df_pandas.groupby("product_category").agg({
"daily_revenue": ["sum", "mean", "std"],
"num_orders": "sum",
"unique_customers": "sum"
}).round(2)
print("\nRésumé par catégorie:")
print(category_summary)
# Métriques globales
total_revenue = df_pandas["daily_revenue"].sum()
total_orders = df_pandas["num_orders"].sum()
print(f"\nChiffre d'affaires total : {total_revenue:,.2f} €")
print(f"Nombre de commandes : {total_orders:,}")
# === Étape 5 : Export des résultats ===
con.execute("""
COPY (SELECT * FROM sales_transformed)
TO 'output/sales_transformed.parquet'
(FORMAT PARQUET, COMPRESSION ZSTD)
""")
print("\nPipeline terminé avec succès!")
Pourquoi ce Pipeline Fonctionne si Bien
Chaque outil joue le rôle pour lequel il est le plus adapté. DuckDB lit efficacement des fichiers multiples avec préfiltrage SQL, évitant de charger des données inutiles en mémoire. Polars effectue les transformations avec des performances optimales grâce à l'évaluation paresseuse et au parallélisme. DuckDB revient pour les agrégations SQL complexes avec window functions (plus naturelles en SQL, avouons-le). Et Pandas finalise l'analyse avec son API riche et sa compatibilité avec les bibliothèques de visualisation.
Le tout communique via Apache Arrow, ce qui rend les conversions entre formats rapides et souvent sans copie mémoire.
Benchmarks et Performances
Comparaison sur Opérations Courantes
Les chiffres valent parfois mieux que les discours. Voici des benchmarks concrets sur des opérations typiques de data engineering avec un dataset de 10 millions de lignes.
import pandas as pd
import polars as pl
import duckdb
import numpy as np
import time
# Génération d'un dataset de test
n_rows = 10_000_000
print(f"Dataset de test : {n_rows:,} lignes\n")
df_polars = pl.DataFrame({
"id": range(n_rows),
"category": [f"cat_{i % 1000}" for i in range(n_rows)],
"value1": np.random.randn(n_rows),
"value2": np.random.randn(n_rows),
"value3": np.random.randint(0, 100, n_rows)
})
df_pandas = df_polars.to_pandas()
# --- Benchmark 1 : Filtrage ---
print("BENCHMARK 1 : Filtrage (value1 > 0.5)")
start = time.time()
result_pd = df_pandas[df_pandas["value1"] > 0.5]
t_pandas = time.time() - start
print(f" Pandas : {t_pandas:.3f}s")
start = time.time()
result_pl = df_polars.filter(pl.col("value1") > 0.5)
t_polars = time.time() - start
print(f" Polars : {t_polars:.3f}s ({t_pandas/t_polars:.1f}x)")
start = time.time()
result_db = duckdb.sql(
"SELECT * FROM df_polars WHERE value1 > 0.5"
).pl()
t_duck = time.time() - start
print(f" DuckDB : {t_duck:.3f}s ({t_pandas/t_duck:.1f}x)")
# --- Benchmark 2 : GroupBy ---
print("\nBENCHMARK 2 : GroupBy avec agrégations multiples")
start = time.time()
df_pandas.groupby("category").agg({
"value1": ["mean", "std", "min", "max"],
"value2": "sum", "value3": "count"
})
t_pandas = time.time() - start
print(f" Pandas : {t_pandas:.3f}s")
start = time.time()
df_polars.group_by("category").agg([
pl.col("value1").mean().alias("v1_mean"),
pl.col("value1").std().alias("v1_std"),
pl.col("value1").min().alias("v1_min"),
pl.col("value1").max().alias("v1_max"),
pl.col("value2").sum().alias("v2_sum"),
pl.col("value3").count().alias("v3_count")
])
t_polars = time.time() - start
print(f" Polars : {t_polars:.3f}s ({t_pandas/t_polars:.1f}x)")
start = time.time()
duckdb.sql("""
SELECT category,
AVG(value1), STDDEV(value1),
MIN(value1), MAX(value1),
SUM(value2), COUNT(value3)
FROM df_polars GROUP BY category
""").pl()
t_duck = time.time() - start
print(f" DuckDB : {t_duck:.3f}s ({t_pandas/t_duck:.1f}x)")
# --- Benchmark 3 : Jointure ---
print("\nBENCHMARK 3 : Jointure entre deux tables")
df_ref = pl.DataFrame({
"category": [f"cat_{i}" for i in range(1000)],
"category_name": [f"Category {i}" for i in range(1000)],
"weight": np.random.rand(1000)
})
df_ref_pd = df_ref.to_pandas()
start = time.time()
df_pandas.merge(df_ref_pd, on="category", how="left")
t_pandas = time.time() - start
print(f" Pandas : {t_pandas:.3f}s")
start = time.time()
df_polars.join(df_ref, on="category", how="left")
t_polars = time.time() - start
print(f" Polars : {t_polars:.3f}s ({t_pandas/t_polars:.1f}x)")
print("\n" + "=" * 50)
print("Polars est généralement 10-30x plus rapide que Pandas.")
print("DuckDB excelle sur les agrégations SQL complexes.")
Consommation Mémoire
La vitesse, c'est bien, mais l'efficacité mémoire est tout aussi cruciale quand on traite de gros volumes. Polars et DuckDB utilisent des représentations mémoire nettement plus efficaces que Pandas avec ses types NumPy traditionnels. Voici un petit comparatif qui parle de lui-même :
import pandas as pd
import polars as pl
def memory_usage_mb(df, library):
if library == "pandas":
return df.memory_usage(deep=True).sum() / 1024**2
elif library == "polars":
return df.estimated_size() / 1024**2
n = 5_000_000
data = {
"integers": range(n),
"floats": [i * 1.5 for i in range(n)],
"strings": [f"string_{i % 10000}" for i in range(n)],
"categories": [f"cat_{i % 100}" for i in range(n)]
}
# Pandas sans optimisation
df_pd = pd.DataFrame(data)
mem_pd = memory_usage_mb(df_pd, "pandas")
print(f"Pandas (types par défaut) : {mem_pd:.2f} MB")
# Pandas avec types catégoriels
df_pd_opt = pd.DataFrame(data)
df_pd_opt["strings"] = df_pd_opt["strings"].astype("category")
df_pd_opt["categories"] = df_pd_opt["categories"].astype("category")
mem_pd_opt = memory_usage_mb(df_pd_opt, "pandas")
print(f"Pandas (optimisé) : {mem_pd_opt:.2f} MB")
print(f" Réduction : {(1 - mem_pd_opt/mem_pd)*100:.1f}%")
# Polars
df_pl = pl.DataFrame(data)
mem_pl = memory_usage_mb(df_pl, "polars")
print(f"Polars : {mem_pl:.2f} MB")
print(f" vs Pandas : {(1 - mem_pl/mem_pd)*100:.1f}% plus efficace")
Bonnes Pratiques pour Pipelines de Production
Gestion de la Mémoire
Pour les datasets volumineux, quelques stratégies s'imposent. Avec Polars, utilisez systématiquement l'évaluation paresseuse (.lazy() et .collect()) pour permettre les optimisations automatiques. Avec DuckDB, profitez de la capacité à interroger des fichiers directement sans tout charger en mémoire. Et quand les données dépassent la RAM, divisez les traitements en chunks.
import polars as pl
import duckdb
# Traitement streaming avec Polars
def process_large_file_polars(file_path):
"""Traite un fichier volumineux avec streaming"""
result = (
pl.scan_csv(file_path)
.filter(pl.col("status") == "active")
.select([
pl.col("id"),
pl.col("amount"),
pl.col("date")
])
.group_by("date")
.agg([
pl.col("amount").sum().alias("daily_total"),
pl.col("id").count().alias("transaction_count")
])
.collect(streaming=True)
)
return result
# Traitement out-of-core avec DuckDB
def process_large_file_duckdb(file_path):
"""Traite un fichier volumineux avec DuckDB"""
result = duckdb.sql(f"""
SELECT
date,
SUM(amount) AS daily_total,
COUNT(id) AS transaction_count
FROM '{file_path}'
WHERE status = 'active'
GROUP BY date
ORDER BY date
""").df()
return result
Validation des Données
En production, la robustesse est essentielle (on le sait tous, mais c'est bon de le rappeler). Implémentez des validations de schéma pour détecter les changements inattendus dans les données source. Ça vous évitera bien des surprises un lundi matin.
import polars as pl
from typing import Dict
def validate_and_load(
file_path: str,
expected_schema: Dict[str, pl.DataType]
) -> pl.DataFrame:
"""Charge et valide un fichier avec gestion d'erreurs"""
try:
df = pl.read_csv(
file_path,
schema=expected_schema,
null_values=["", "NULL", "N/A"]
)
# Validations business
assert len(df) > 0, "Fichier vide"
assert df["amount"].null_count() == 0, \
"Valeurs manquantes dans 'amount'"
assert (df["amount"] >= 0).all(), \
"Valeurs négatives dans 'amount'"
print(f"Fichier chargé : {len(df):,} lignes")
print(f"Période : {df['date'].min()} à {df['date'].max()}")
return df
except Exception as e:
print(f"Erreur lors du chargement : {e}")
raise
# Utilisation
schema = {
"id": pl.Int64,
"date": pl.Date,
"amount": pl.Float64,
"category": pl.Utf8
}
Tests Unitaires
Testez vos pipelines de données comme du code applicatif. Créez des fixtures avec des données de test représentatives et testez les cas limites. Ça a l'air évident, mais en pratique, peu de data engineers le font systématiquement.
import polars as pl
def transform_sales_data(df: pl.DataFrame) -> pl.DataFrame:
"""Fonction de transformation à tester"""
return (
df
.filter(pl.col("amount") > 0)
.with_columns([
(pl.col("amount") * pl.col("quantity")).alias("total"),
pl.col("date").dt.year().alias("year")
])
.group_by("year")
.agg([
pl.col("total").sum().alias("yearly_total"),
pl.col("customer_id").n_unique().alias("unique_customers")
])
)
# Test : les montants négatifs sont bien filtrés
def test_transform_filters_negative_amounts():
df_input = pl.DataFrame({
"amount": [100, -50, 200],
"quantity": [1, 1, 2],
"date": ["2026-01-01", "2026-01-02", "2026-01-03"],
"customer_id": [1, 2, 3]
})
result = transform_sales_data(df_input)
assert len(result) == 1
assert result["yearly_total"][0] == 500 # 100*1 + 200*2
# Test : gestion d'un DataFrame vide
def test_transform_handles_empty_dataframe():
df_input = pl.DataFrame({
"amount": [],
"quantity": [],
"date": [],
"customer_id": []
})
result = transform_sales_data(df_input)
assert len(result) == 0
Conclusion : Quel Avenir pour le Traitement de Données en Python ?
L'écosystème Python pour le traitement de données a atteint une maturité remarquable en 2026. Et ce qui est fascinant, c'est qu'on n'assiste pas à une guerre des bibliothèques, mais plutôt à une convergence vers des workflows hybrides qui combinent intelligemment les forces de chaque outil.
Pandas reste la fondation incontournable. Sa maturité, sa documentation et son intégration dans l'écosystème scientifique n'ont pas d'équivalent. Les améliorations de Pandas 3.0 avec PyArrow et Copy-on-Write ont considérablement réduit l'écart de performance, tout en préservant l'API que des millions de développeurs connaissent.
Polars s'est imposé comme le champion de la performance. Son architecture Rust, son évaluation paresseuse sophistiquée et son API expressive en font le choix évident pour les traitements intensifs. Et les gains de 10 à 30x par rapport à Pandas ne sont pas anecdotiques : ils permettent de traiter des volumes qui nécessitaient auparavant des clusters distribués.
DuckDB a révolutionné l'analyse avec son moteur SQL embarqué. Sa capacité à interroger directement des fichiers et des DataFrames, combinée à ses performances exceptionnelles sur les requêtes analytiques, en fait un outil indispensable.
L'interopérabilité entre ces trois outils est remarquable grâce à Apache Arrow. Les conversions sont rapides et souvent sans copie mémoire, ce qui permet de construire des pipelines fluides où chaque outil intervient là où il excelle.
Pour les data engineers et data scientists, la maîtrise de ces trois outils n'est plus optionnelle — c'est devenu essentiel. L'avenir appartient aux architectures hybrides qui combinent pragmatisme et intelligence pour extraire le maximum de valeur des données. En adoptant cette approche multi-outils dès maintenant, vous vous positionnez idéalement pour relever les défis du traitement de données à grande échelle en Python.