🟩 Niveau 2 : Intermédiaire
Polars pour Data Engineers
Bienvenue dans ce module où tu vas découvrir Polars, la bibliothèque DataFrame ultra-rapide qui révolutionne le traitement de données en Python. Tu apprendras pourquoi Polars surpasse Pandas, comment exploiter son moteur d'exécution lazy, et comment construire des pipelines ETL performants !
Prérequis
| Niveau | Compétence |
|---|---|
| ✅ Requis | Connaissances de base en Python |
| ✅ Requis | Avoir utilisé Pandas (même basiquement) |
| 💡 Recommandé | Avoir suivi les modules précédents du bootcamp |
Objectifs du module
À la fin de ce module, tu seras capable de :
- Comprendre pourquoi Polars est 5-100x plus rapide que Pandas
- Maîtriser l'architecture columnar et le format Apache Arrow
- Utiliser les expressions Polars pour des transformations efficaces
- Exploiter l'exécution Lazy pour des pipelines optimisés
- Migrer du code Pandas vers Polars
- Construire des pipelines ETL performants en production
1. Polars vs Pandas : Pourquoi changer ?
Avant de plonger dans Polars, comprenons pourquoi cette bibliothèque existe et ce qu'elle apporte.
1.1 Les limitations de Pandas
Pandas est formidable pour l'exploration de données, mais il a des limitations structurelles :
| Limitation | Explication | Impact |
|---|---|---|
| Single-threaded | Le GIL Python bloque le parallélisme | N'utilise qu'1 CPU |
| Row-based en mémoire | Données stockées par ligne | Cache CPU inefficace |
| Eager execution | Chaque opération s'exécute immédiatement | Pas d'optimisation globale |
| Copies fréquentes | Beaucoup d'opérations copient les données | RAM x2 ou x3 |
| Mémoire gourmande | ~5-10x la taille du fichier | Limite les gros datasets |
1.2 Les forces de Polars
| Aspect | Pandas | Polars |
|---|---|---|
| Backend | NumPy (C) | Rust 🦀 |
| Threading | Single (GIL) | Multi-threaded |
| Mémoire | Row-based | Columnar (Arrow) |
| Execution | Eager only | Eager + Lazy |
| Vitesse | Baseline | 5-100x plus rapide |
| Out-of-core | ❌ | ✅ (streaming) |
| Optimiseur | ❌ | ✅ Query planner |
💡 En résumé : Polars est conçu dès le départ pour la performance et les gros volumes, là où Pandas a été conçu pour l'exploration interactive.
ℹ️ Le savais-tu ?
Polars a été créé en 2020 par Ritchie Vink, un ingénieur néerlandais frustré par la lenteur de Pandas.
Le nom "Polars" fait référence à l'ours polaire (🐻❄️) — un clin d'œil à Pandas (🐼) tout en étant plus rapide et adapté aux environnements "froids" (haute performance).
Polars est écrit en Rust, un langage réputé pour sa vitesse et sa sécurité mémoire.
1.3 Benchmark concret
Comparons Pandas et Polars sur une opération simple : lire un CSV et faire une agrégation.
pythonVoir le code
# Créer un fichier de test
import random
import csv
import os
os.makedirs("data", exist_ok=True)
# Générer 500K lignes
categories = ["Electronics", "Clothing", "Food", "Books", "Sports"]
n_rows = 500_000
with open("data/benchmark.csv", "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["id", "category", "amount", "quantity"])
for i in range(n_rows):
writer.writerow([
i,
random.choice(categories),
round(random.uniform(10, 1000), 2),
random.randint(1, 100)
])
print(f"✅ Fichier créé : data/benchmark.csv ({n_rows:,} lignes)")pythonVoir le code
import pandas as pd
import time
# Benchmark Pandas
start = time.time()
df_pandas = pd.read_csv("data/benchmark.csv")
result_pandas = (
df_pandas
.groupby("category")
.agg({"amount": "sum", "quantity": "mean"})
.reset_index()
)
pandas_time = time.time() - start
print(f"🐼 Pandas : {pandas_time:.3f} secondes")
print(result_pandas)pythonVoir le code
import polars as pl
import time
# Benchmark Polars (Eager)
start = time.time()
df_polars = pl.read_csv("data/benchmark.csv")
result_polars = (
df_polars
.group_by("category")
.agg(
pl.col("amount").sum().alias("amount_sum"),
pl.col("quantity").mean().alias("quantity_mean")
)
)
polars_time = time.time() - start
print(f"🐻❄️ Polars : {polars_time:.3f} secondes")
print(f"⚡ Polars est {pandas_time/polars_time:.1f}x plus rapide !")
print(result_polars)pythonVoir le code
# Benchmark Polars (Lazy) - encore plus rapide !
start = time.time()
result_lazy = (
pl.scan_csv("data/benchmark.csv") # Lazy !
.group_by("category")
.agg(
pl.col("amount").sum().alias("amount_sum"),
pl.col("quantity").mean().alias("quantity_mean")
)
.collect() # Exécution optimisée
)
lazy_time = time.time() - start
print(f"🚀 Polars Lazy : {lazy_time:.3f} secondes")
print(f"⚡ Polars Lazy est {pandas_time/lazy_time:.1f}x plus rapide que Pandas !")2. Comprendre l'architecture de Polars
Pour bien utiliser Polars, il faut comprendre pourquoi il est si rapide.
2.1 Columnar vs Row-based
ROW-BASED (Pandas) COLUMNAR (Polars)
══════════════════ ═════════════════
┌─────┬──────┬─────┐ ┌───────────────────┐
│ id │ name │ age │ │ id: [1, 2, 3] │
├─────┼──────┼─────┤ ├───────────────────┤
│ 1 │ Ana │ 25 │ ← Ligne 1 │ name: [A, B, C] │
├─────┼──────┼─────┤ ├───────────────────┤
│ 2 │ Bob │ 30 │ ← Ligne 2 │ age: [25, 30, 22]│
├─────┼──────┼─────┤ └───────────────────┘
│ 3 │ Cat │ 22 │ ← Ligne 3 ↑
└─────┴──────┴─────┘ Colonnes contiguës
↑ en mémoire
Lignes contiguës
en mémoire
Pourquoi columnar est plus rapide ?
| Avantage | Explication |
|---|---|
| Cache CPU | Données contiguës = moins de cache misses |
| SIMD | Opérations vectorisées sur colonnes entières |
| Compression | Colonnes homogènes = meilleure compression |
| Sélection | Lire seulement les colonnes nécessaires |
2.2 Apache Arrow : le format sous-jacent
Polars utilise Apache Arrow comme format mémoire :
| Avantage | Description |
|---|---|
| Zero-copy | Partage de données sans copie |
| Interopérabilité | Compatible Spark, DuckDB, PyArrow |
| Standardisé | Format ouvert et documenté |
2.3 Eager vs Lazy execution
| Mode | Description | Quand l'utiliser |
|---|---|---|
| Eager | Exécute immédiatement chaque opération | Exploration, debug, petits datasets |
| Lazy | Construit un plan, optimise, puis exécute | Production, gros fichiers, pipelines |
# Eager : résultat immédiat
df = pl.read_csv("data.csv") # Lit maintenant
df = df.filter(pl.col("x") > 5) # Filtre maintenant
# Lazy : plan d'exécution
lf = pl.scan_csv("data.csv") # Crée un plan
lf = lf.filter(pl.col("x") > 5) # Ajoute au plan
df = lf.collect() # Exécute tout (optimisé)
2.4 Query Optimizer
Le Query Optimizer de Polars applique automatiquement des optimisations :
| Optimisation | Description |
|---|---|
| Predicate pushdown | Filtres appliqués le plus tôt possible |
| Projection pruning | Colonnes inutiles jamais lues |
| Common subexpression | Calculs redondants factorisés |
| Parallelization | Opérations distribuées sur tous les CPUs |
PLAN ORIGINAL PLAN OPTIMISÉ
═════════════ ═════════════
scan_csv(all cols) scan_csv(only needed cols)
│ │
▼ ▼
with_columns(...) filter(amount > 100) ← Pushdown!
│ │
▼ ▼
filter(amount > 100) with_columns(...)
│ │
▼ ▼
result result
3. Installation & Configuration
Installation
# Installation de base
pip install polars
# Avec toutes les features (recommandé)
pip install 'polars[all]'
# Features spécifiques
pip install 'polars[pyarrow,pandas,numpy,fsspec]'
Vérification
pythonVoir le code
import polars as pl
print(f"✅ Polars version : {pl.__version__}")
# Configuration de l'affichage
pl.Config.set_tbl_rows(10) # Lignes affichées
pl.Config.set_tbl_cols(12) # Colonnes affichées
pl.Config.set_fmt_str_lengths(50) # Longueur des strings
# Voir le nombre de threads utilisés
print(f"🔧 Threads disponibles : {pl.thread_pool_size()}")4. Charger & Exporter des données
4.1 Formats supportés
| Format | Read (Eager) | Scan (Lazy) | Write |
|---|---|---|---|
| CSV | read_csv() |
scan_csv() |
write_csv() |
| Parquet | read_parquet() |
scan_parquet() |
write_parquet() |
| JSON | read_json() |
scan_ndjson() |
write_json() |
| Excel | read_excel() |
❌ | write_excel() |
| Database | read_database() |
❌ | ❌ |
| IPC/Feather | read_ipc() |
scan_ipc() |
write_ipc() |
4.2 Lecture Eager vs Lazy
pythonVoir le code
import polars as pl
# ============ EAGER (tout en mémoire) ============
df = pl.read_csv("data/benchmark.csv")
print("Eager - Type:", type(df))
print(df.head(3))
print("\n" + "="*50 + "\n")
# ============ LAZY (plan d'exécution) ============
lf = pl.scan_csv("data/benchmark.csv")
print("Lazy - Type:", type(lf))
print(lf) # Affiche le plan, pas les donnéespythonVoir le code
# Créer plusieurs fichiers pour l'exemple
import os
os.makedirs("data/multi", exist_ok=True)
for i in range(3):
pl.DataFrame({
"id": range(i*100, (i+1)*100),
"value": [i*10 + j for j in range(100)]
}).write_csv(f"data/multi/file_{i}.csv")
print("✅ Fichiers créés")
# Lire plusieurs fichiers avec glob pattern
lf = pl.scan_csv("data/multi/*.csv")
print(f"\nNombre de lignes : {lf.collect().height}")pythonVoir le code
# Écriture
df = pl.DataFrame({
"name": ["Alice", "Bob", "Charlie"],
"age": [25, 30, 35],
"city": ["Paris", "Lyon", "Marseille"]
})
# CSV
df.write_csv("data/output.csv")
# Parquet (recommandé pour la production)
df.write_parquet("data/output.parquet")
# JSON
df.write_json("data/output.json")
print("✅ Fichiers exportés")
# Vérifier avec Parquet
df_parquet = pl.read_parquet("data/output.parquet")
print(df_parquet)5. Expressions Polars — Le cœur du moteur
Les expressions sont ce qui rend Polars si puissant. C'est un changement de paradigme par rapport à Pandas.
5.1 Philosophie : tout est expression
# ❌ Pandas : opérations sur colonnes
df["new_col"] = df["a"] + df["b"]
# ✅ Polars : expressions
df.with_columns(
(pl.col("a") + pl.col("b")).alias("new_col")
)
5.2 Expressions de base
| Expression | Description | Exemple |
|---|---|---|
pl.col("x") |
Sélectionner une colonne | pl.col("amount") |
pl.col("x", "y") |
Plusieurs colonnes | pl.col("a", "b", "c") |
pl.all() |
Toutes les colonnes | df.select(pl.all()) |
pl.exclude("x") |
Toutes sauf x | pl.exclude("id") |
pl.lit(42) |
Valeur littérale | pl.lit("constant") |
pl.col("*") |
Toutes (autre syntaxe) | pl.col("*") |
pythonVoir le code
import polars as pl
df = pl.DataFrame({
"name": ["Alice", "Bob", "Charlie", "Diana"],
"age": [25, 30, 35, 28],
"salary": [50000, 60000, 75000, 55000],
"department": ["IT", "HR", "IT", "Finance"]
})
print("DataFrame original :")
print(df)
# Sélectionner des colonnes avec expressions
print("\nSélection avec expressions :")
print(
df.select(
pl.col("name"),
pl.col("salary") / 12, # Salaire mensuel
)
)pythonVoir le code
# Expressions conditionnelles : when/then/otherwise
print("Expressions conditionnelles :")
print(
df.with_columns(
pl.when(pl.col("age") >= 30)
.then(pl.lit("Senior"))
.otherwise(pl.lit("Junior"))
.alias("level"),
pl.when(pl.col("salary") > 60000)
.then(pl.lit("High"))
.when(pl.col("salary") > 50000)
.then(pl.lit("Medium"))
.otherwise(pl.lit("Low"))
.alias("salary_band")
)
)pythonVoir le code
# Chaînage d'expressions
print("Chaînage d'expressions :")
print(
df.with_columns(
# String operations
pl.col("name").str.to_uppercase().alias("NAME_UPPER"),
pl.col("name").str.len_chars().alias("name_length"),
# Math operations
(pl.col("salary") * 1.1).round(2).alias("salary_raised"),
)
)6. Manipulations de données essentielles
6.1 Sélection de colonnes
pythonVoir le code
df = pl.read_csv("data/benchmark.csv")
# Sélection simple
print("Sélection simple :")
print(df.select("category", "amount").head(3))
# Sélection avec transformation
print("\nSélection avec transformation :")
print(
df.select(
pl.col("category"),
(pl.col("amount") * pl.col("quantity")).alias("total")
).head(3)
)
# Sélection par type
print("\nColonnes numériques uniquement :")
print(df.select(pl.col(pl.Float64, pl.Int64)).head(3))6.2 Filtrage
pythonVoir le code
# Filtre simple
print("Filtre simple (amount > 500) :")
print(df.filter(pl.col("amount") > 500).head(3))
# Filtres multiples (AND)
print("\nFiltres multiples (AND) :")
print(
df.filter(
(pl.col("amount") > 500) &
(pl.col("category") == "Electronics")
).head(3)
)
# Filtres multiples (OR)
print("\nFiltres multiples (OR) :")
print(
df.filter(
(pl.col("category") == "Electronics") |
(pl.col("category") == "Books")
).head(3)
)
# Filtre avec is_in
print("\nFiltre avec is_in :")
print(
df.filter(
pl.col("category").is_in(["Electronics", "Books"])
).head(3)
)6.3 Ajout / modification de colonnes
pythonVoir le code
print("Ajout de colonnes :")
result = df.with_columns(
# Calcul
(pl.col("amount") * pl.col("quantity")).alias("total"),
# Valeur constante
pl.lit("USD").alias("currency"),
# Transformation de colonne existante
pl.col("category").str.to_uppercase().alias("CATEGORY"),
# Conditionnel
pl.when(pl.col("amount") > 500)
.then(pl.lit("High"))
.otherwise(pl.lit("Low"))
.alias("amount_level")
)
print(result.head(5))6.4 GroupBy & Aggregations
pythonVoir le code
print("GroupBy simple :")
print(
df.group_by("category").agg(
pl.col("amount").sum().alias("total_amount"),
pl.col("amount").mean().alias("avg_amount"),
pl.col("amount").max().alias("max_amount"),
pl.len().alias("count")
).sort("total_amount", descending=True)
)pythonVoir le code
# Aggregations avancées
print("Aggregations avancées :")
print(
df.group_by("category").agg(
# Statistiques
pl.col("amount").mean().alias("avg"),
pl.col("amount").std().alias("std"),
pl.col("amount").quantile(0.5).alias("median"),
# Comptages conditionnels
(pl.col("amount") > 500).sum().alias("high_amount_count"),
# Premier/Dernier
pl.col("amount").first().alias("first_amount"),
)
)6.5 Tri, renommage, suppression
pythonVoir le code
# Tri
print("Tri décroissant :")
print(df.sort("amount", descending=True).head(3))
# Tri multiple
print("\nTri multiple :")
print(df.sort(["category", "amount"], descending=[True, False]).head(5))
# Renommer
print("\nRenommer :")
print(df.rename({"amount": "montant", "quantity": "quantite"}).head(2))
# Supprimer des colonnes
print("\nSupprimer colonnes :")
print(df.drop("id").head(2))6.6 Joins
pythonVoir le code
# Créer des DataFrames pour les joins
orders = pl.DataFrame({
"order_id": [1, 2, 3, 4],
"customer_id": [101, 102, 101, 103],
"amount": [100, 200, 150, 300]
})
customers = pl.DataFrame({
"customer_id": [101, 102, 104],
"name": ["Alice", "Bob", "Diana"]
})
print("Orders:", orders)
print("\nCustomers:", customers)
# Inner join
print("\nInner Join :")
print(orders.join(customers, on="customer_id", how="inner"))
# Left join
print("\nLeft Join :")
print(orders.join(customers, on="customer_id", how="left"))6.7 Dates et timestamps
pythonVoir le code
from datetime import datetime, date
df_dates = pl.DataFrame({
"event": ["A", "B", "C", "D"],
"timestamp": [
datetime(2024, 1, 15, 10, 30),
datetime(2024, 3, 20, 14, 45),
datetime(2024, 6, 5, 9, 0),
datetime(2024, 12, 25, 18, 30)
]
})
print("DataFrame avec dates :")
print(df_dates)
print("\nExtractions de dates :")
print(
df_dates.with_columns(
pl.col("timestamp").dt.year().alias("year"),
pl.col("timestamp").dt.month().alias("month"),
pl.col("timestamp").dt.day().alias("day"),
pl.col("timestamp").dt.hour().alias("hour"),
pl.col("timestamp").dt.weekday().alias("weekday"),
pl.col("timestamp").dt.strftime("%Y-%m-%d").alias("date_str"),
)
)7. Lazy Execution — Le Game Changer
🎯 C'est ce qui rend Polars adapté à la production et aux gros volumes.
7.1 Créer un LazyFrame
pythonVoir le code
# Depuis un fichier (recommandé)
lf = pl.scan_csv("data/benchmark.csv")
print("Type:", type(lf))
print("\nLazyFrame (pas encore exécuté) :")
print(lf)pythonVoir le code
# Depuis un DataFrame existant
df = pl.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
lf = df.lazy()
print("Converti en LazyFrame:", type(lf))7.2 Construire le pipeline
pythonVoir le code
# Pipeline complet en Lazy
pipeline = (
pl.scan_csv("data/benchmark.csv")
.filter(pl.col("amount") > 100)
.with_columns(
(pl.col("amount") * pl.col("quantity")).alias("total"),
pl.col("category").str.to_uppercase().alias("CATEGORY")
)
.group_by("CATEGORY")
.agg(
pl.col("total").sum().alias("total_revenue"),
pl.len().alias("transaction_count")
)
.sort("total_revenue", descending=True)
)
print("Pipeline défini (pas encore exécuté) :")
print(pipeline)
print("\n⚠️ Rien n'a été lu ou calculé !")7.3 Exécuter avec .collect()
pythonVoir le code
import time
start = time.time()
result = pipeline.collect() # MAINTENANT ça s'exécute
print(f"⏱️ Temps d'exécution : {time.time() - start:.3f}s")
print("\nRésultat :")
print(result)7.4 Voir le plan d'exécution
pythonVoir le code
# Plan logique (ce que tu as écrit)
print("=== PLAN LOGIQUE ===")
print(pipeline.explain())
print("\n" + "="*60 + "\n")
# Plan optimisé (ce que Polars exécute réellement)
print("=== PLAN OPTIMISÉ ===")
print(pipeline.explain(optimized=True))🖼️ Schéma : Pipeline Lazy
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ scan_csv() │────▶│ filter() │────▶│with_columns()│───▶│ group_by() │
│ (plan) │ │ (plan) │ │ (plan) │ │ (plan) │
└─────────────┘ └─────────────┘ └─────────────┘ └──────┬──────┘
│
▼
┌─────────────┐
│ collect() │
└──────┬──────┘
│
┌──────────────┴──────────────┐
│ Query Optimizer │
│ • Predicate pushdown │
│ • Column pruning │
│ • Parallel execution │
└──────────────┬──────────────┘
│
▼
┌─────────────┐
│ DataFrame │
│ (résultat) │
└─────────────┘
8. Migration Pandas → Polars
8.1 Tableau de correspondance
| Opération | Pandas | Polars |
|---|---|---|
| Lire CSV | pd.read_csv() |
pl.read_csv() / pl.scan_csv() |
| Lire Parquet | pd.read_parquet() |
pl.read_parquet() / pl.scan_parquet() |
| Sélection colonne | df["col"] |
df.select("col") |
| Plusieurs colonnes | df[["a", "b"]] |
df.select("a", "b") |
| Filtre | df[df["x"] > 5] |
df.filter(pl.col("x") > 5) |
| Nouvelle colonne | df["new"] = df["a"] + 1 |
df.with_columns((pl.col("a") + 1).alias("new")) |
| GroupBy | df.groupby("x").agg({"y": "sum"}) |
df.group_by("x").agg(pl.col("y").sum()) |
| Tri | df.sort_values("x") |
df.sort("x") |
| Renommer | df.rename(columns={"a": "b"}) |
df.rename({"a": "b"}) |
| Drop | df.drop(columns=["x"]) |
df.drop("x") |
| Reset index | df.reset_index() |
N/A (pas d'index) |
| Apply | df.apply(func) |
df.map_rows(func) ⚠️ éviter |
8.2 Interopérabilité
pythonVoir le code
import pandas as pd
import polars as pl
# Créer un DataFrame Pandas
pandas_df = pd.DataFrame({
"name": ["Alice", "Bob"],
"age": [25, 30]
})
# Pandas → Polars
polars_df = pl.from_pandas(pandas_df)
print("Pandas → Polars :")
print(polars_df)
# Polars → Pandas
back_to_pandas = polars_df.to_pandas()
print("\nPolars → Pandas :")
print(back_to_pandas)8.3 Différences clés à retenir
| Aspect | Pandas | Polars |
|---|---|---|
| Index | ✅ Index par défaut | ❌ Pas d'index |
| Modification in-place | ✅ inplace=True |
❌ Toujours immutable |
| Typage | Flexible | Strict |
| NaN vs null | NaN (float) | null (natif) |
| Chaînage | Limité | Naturel et optimisé |
pythonVoir le code
# Exemple de migration complète
# ============ VERSION PANDAS ============
# df = pd.read_csv("data.csv")
# df = df[df["amount"] > 100]
# df["total"] = df["amount"] * df["quantity"]
# result = df.groupby("category").agg({"total": "sum"}).reset_index()
# ============ VERSION POLARS (Eager) ============
result_eager = (
pl.read_csv("data/benchmark.csv")
.filter(pl.col("amount") > 100)
.with_columns(
(pl.col("amount") * pl.col("quantity")).alias("total")
)
.group_by("category")
.agg(pl.col("total").sum())
)
# ============ VERSION POLARS (Lazy - recommandé) ============
result_lazy = (
pl.scan_csv("data/benchmark.csv")
.filter(pl.col("amount") > 100)
.with_columns(
(pl.col("amount") * pl.col("quantity")).alias("total")
)
.group_by("category")
.agg(pl.col("total").sum())
.collect()
)
print("Résultat :")
print(result_lazy)9. Bonnes pratiques & Erreurs fréquentes
❌ Erreurs fréquentes
| Erreur | Problème | Solution |
|---|---|---|
.apply() sur chaque ligne |
Extrêmement lent | Utiliser expressions natives |
df["col"] style Pandas |
Ne fonctionne pas | df.select("col") ou pl.col() |
read_csv() sur 100 fichiers |
Lent, beaucoup de RAM | scan_csv("*.csv") + glob |
Oublier .collect() |
Pas d'exécution | Toujours .collect() à la fin |
| Mélanger eager/lazy | Erreurs de type | Rester cohérent dans le pipeline |
| Pas d'alias sur les expressions | Noms de colonnes illisibles | Toujours .alias("nom") |
pythonVoir le code
# ❌ MAUVAIS : apply() ligne par ligne
# df.map_rows(lambda row: row[0] * 2) # TRÈS LENT
# ✅ BON : expression native
df = pl.DataFrame({"x": [1, 2, 3]})
result = df.with_columns((pl.col("x") * 2).alias("x_doubled"))
print("✅ Expression native :")
print(result)pythonVoir le code
# ❌ MAUVAIS : read_csv sur plusieurs fichiers séparément
# dfs = [pl.read_csv(f) for f in files] # Pas optimisé
# ✅ BON : scan_csv avec glob
lf = pl.scan_csv("data/multi/*.csv")
print("✅ Scan avec glob :")
print(lf.collect())✅ Bonnes pratiques
| Pratique | Pourquoi |
|---|---|
| Utiliser Lazy en production | Optimisation automatique |
| Préférer Parquet | 10x plus rapide que CSV, compression |
| Chaîner les expressions | Plus lisible, plus optimisé |
Éviter .apply() |
Utiliser expressions natives |
Profiler avec .explain() |
Comprendre l'exécution |
Toujours .alias() |
Noms de colonnes explicites |
scan_* pour gros fichiers |
Lazy = optimisations |
| Streaming pour > RAM | collect(streaming=True) |
Quiz de fin de module
Réponds aux questions suivantes pour vérifier tes acquis.
❓ Q1. Quel est le principal avantage de l'architecture columnar de Polars ?
a) Plus facile à lire pour les humains
b) Opérations vectorisées plus rapides et meilleure utilisation du cache CPU
c) Compatible avec Excel
d) Utilise moins de colonnes
💡 Voir la réponse
✅ Réponse : b — Le stockage columnar permet des opérations vectorisées (SIMD) et une meilleure utilisation du cache CPU car les données d'une colonne sont contiguës en mémoire.
❓ Q2. Quelle est la différence entre pl.read_csv() et pl.scan_csv() ?
a) read_csv est plus rapide
b) scan_csv crée un LazyFrame et permet l'optimisation
c) scan_csv ne supporte pas les gros fichiers
d) Aucune différence
💡 Voir la réponse
✅ Réponse : b — scan_csv crée un LazyFrame (plan d'exécution) qui sera optimisé avant exécution, tandis que read_csv charge immédiatement tout en mémoire.
❓ Q3. Comment ajouter une nouvelle colonne en Polars ?
a) df["new"] = df["old"] * 2
b) df.with_columns((pl.col("old") * 2).alias("new"))
c) df.add_column("new", df["old"] * 2)
d) df.insert("new", df["old"] * 2)
💡 Voir la réponse
✅ Réponse : b — En Polars, on utilise with_columns() avec des expressions. La syntaxe df["col"] style Pandas ne fonctionne pas.
❓ Q4. Que fait le Query Optimizer avec "predicate pushdown" ?
a) Supprime les colonnes inutiles
b) Applique les filtres le plus tôt possible dans le pipeline
c) Parallélise les calculs
d) Compresse les données
💡 Voir la réponse
✅ Réponse : b — Le predicate pushdown déplace les filtres le plus tôt possible, réduisant ainsi la quantité de données à traiter dans les étapes suivantes.
❓ Q5. Quand utiliser .collect() ?
a) Après chaque opération
b) À la fin du pipeline Lazy pour déclencher l'exécution
c) Pour convertir en Pandas
d) Pour écrire un fichier
💡 Voir la réponse
✅ Réponse : b — .collect() déclenche l'exécution d'un LazyFrame et retourne un DataFrame. Sans .collect(), rien n'est calculé.
❓ Q6. Pourquoi éviter .apply() en Polars ?
a) Ce n'est pas supporté
b) C'est lent car ça passe par Python pour chaque ligne
c) Ça modifie les données en place
d) Ça consomme trop de mémoire
💡 Voir la réponse
✅ Réponse : b — .apply() (ou map_rows) passe par Python pour chaque ligne, perdant tous les avantages du moteur Rust vectorisé. Préférer les expressions natives.
❓ Q7. Quel format de fichier est recommandé en production avec Polars ?
a) CSV
b) JSON
c) Parquet
d) Excel
💡 Voir la réponse
✅ Réponse : c — Parquet est columnar (comme Polars), compressé, et supporte les types. Il est 10x+ plus rapide que CSV.
❓ Q8. Comment voir le plan d'exécution optimisé d'un LazyFrame ?
a) lf.show_plan()
b) lf.explain(optimized=True)
c) lf.describe()
d) print(lf)
💡 Voir la réponse
✅ Réponse : b — .explain(optimized=True) affiche le plan d'exécution après les optimisations du Query Optimizer.
Mini-projet : Pipeline ETL Polars
Objectif
Construire un pipeline ETL complet en mode Lazy qui :
- Lit plusieurs fichiers CSV
- Nettoie et transforme les données
- Agrège par catégorie et période
- Exporte en Parquet
Architecture
data/raw/*.csv
│
▼
┌─────────────────┐
│ scan_csv() │ Lazy read (glob pattern)
└────────┬────────┘
│
▼
┌─────────────────┐
│ filter() │ Nettoyage (nulls, invalides)
└────────┬────────┘
│
▼
┌─────────────────┐
│ with_columns() │ Enrichissement
└────────┬────────┘
│
▼
┌─────────────────┐
│ group_by() │ Agrégation
└────────┬────────┘
│
▼
┌─────────────────┐
│ collect() │ Exécution optimisée
└────────┬────────┘
│
▼
data/processed/output.parquet
Structure projet
polars-etl-project/
├── data/
│ ├── raw/
│ │ ├── transactions_01.csv
│ │ ├── transactions_02.csv
│ │ └── transactions_03.csv
│ └── processed/
│ └── output.parquet
├── src/
│ └── etl_pipeline.py
└── requirements.txt
pythonVoir le code
# Setup : créer les données de test
import polars as pl
import random
from datetime import datetime, timedelta
import os
os.makedirs("data/raw", exist_ok=True)
os.makedirs("data/processed", exist_ok=True)
categories = ["Electronics", "Clothing", "Food", "Books", "Sports"]
base_date = datetime(2024, 1, 1)
# Générer 3 fichiers CSV
for file_num in range(1, 4):
n_rows = 10000
data = {
"transaction_id": range(file_num * 10000, file_num * 10000 + n_rows),
"timestamp": [base_date + timedelta(days=random.randint(0, 365)) for _ in range(n_rows)],
"category": [random.choice(categories) for _ in range(n_rows)],
"amount": [round(random.uniform(-50, 1000), 2) for _ in range(n_rows)], # Certains négatifs !
"quantity": [random.randint(0, 100) for _ in range(n_rows)], # Certains à 0 !
"customer_id": [random.randint(1000, 9999) for _ in range(n_rows)]
}
df = pl.DataFrame(data)
df.write_csv(f"data/raw/transactions_{file_num:02d}.csv")
print("✅ Données de test créées (3 fichiers x 10,000 lignes)")pythonVoir le code
import polars as pl
import time
print("🚀 Démarrage du pipeline ETL Polars...\n")
start = time.time()
# ============ PIPELINE LAZY ============
result = (
# 1. EXTRACT : Lire tous les CSV avec glob pattern
pl.scan_csv("data/raw/*.csv")
# 2. CLEAN : Filtrer les données invalides
.filter(
(pl.col("amount") > 0) & # Montants positifs
(pl.col("quantity") > 0) & # Quantités positives
(pl.col("customer_id").is_not_null()) # Pas de null
)
# 3. TRANSFORM : Enrichir les données
.with_columns(
# Calculer le total
(pl.col("amount") * pl.col("quantity")).alias("total_revenue"),
# Extraire année et mois
pl.col("timestamp").str.to_datetime().dt.year().alias("year"),
pl.col("timestamp").str.to_datetime().dt.month().alias("month"),
# Catégoriser les montants
pl.when(pl.col("amount") > 500)
.then(pl.lit("High"))
.when(pl.col("amount") > 100)
.then(pl.lit("Medium"))
.otherwise(pl.lit("Low"))
.alias("amount_tier"),
# Uppercase category
pl.col("category").str.to_uppercase().alias("category_upper")
)
# 4. AGGREGATE : Par catégorie et mois
.group_by(["year", "month", "category_upper"])
.agg(
pl.col("total_revenue").sum().alias("total_revenue"),
pl.col("total_revenue").mean().alias("avg_revenue"),
pl.len().alias("transaction_count"),
pl.col("customer_id").n_unique().alias("unique_customers"),
(pl.col("amount_tier") == "High").sum().alias("high_value_count")
)
# 5. SORT
.sort(["year", "month", "total_revenue"], descending=[False, False, True])
# 6. EXECUTE
.collect()
)
execution_time = time.time() - start
print(f"Pipeline exécuté en {execution_time:.3f} secondes")
print(f"Résultat : {result.height} lignes, {result.width} colonnes\n")
# Afficher un aperçu
print("Aperçu des résultats :")
print(result.head(10))
# 7. EXPORT en Parquet
result.write_parquet("data/processed/monthly_summary.parquet")
print("\n Résultat exporté : data/processed/monthly_summary.parquet")pythonVoir le code
# Vérifier le fichier Parquet
print("Lecture du fichier Parquet exporté :")
df_check = pl.read_parquet("data/processed/monthly_summary.parquet")
print(df_check.describe())📚 Ressources pour aller plus loin
🌐 Documentation officielle
- Polars User Guide — Documentation complète
- Polars API Reference — Référence API
- Polars GitHub — Code source
📖 Tutoriels & Articles
- Polars vs Pandas Benchmark — Benchmarks officiels
- Modern Polars — Guide approfondi
🔧 Outils complémentaires
➡️ Prochaine étape
Maintenant que tu maîtrises Polars, découvrons d'autres outils haute performance pour Python !
👉 Module suivant : 18_high_performance_python — Python Haute Performance
Tu vas apprendre :
- Dask : parallélisation de Pandas/NumPy
- Vaex : traitement out-of-core
- multiprocessing : parallélisme CPU
- concurrent.futures : ThreadPool et ProcessPool
- async/await : I/O asynchrone
🎉 Félicitations ! Tu as terminé le module Polars pour Data Engineers.
pythonVoir le code
# Nettoyage des fichiers temporaires (optionnel)
import shutil
import os
# Décommenter pour nettoyer
# if os.path.exists("data"):
# shutil.rmtree("data")
# print("🧹 Dossier data/ supprimé")