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.

📖 Site officiel Polars

1.3 Benchmark concret

Comparons Pandas et Polars sur une opération simple : lire un CSV et faire une agrégation.

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

Voir 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

Voir 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ées
Voir 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}")
Voir 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("*")
Voir 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
    )
)
Voir 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")
    )
)
Voir 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

Voir 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

Voir 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

Voir 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

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

Voir 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

Voir 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

Voir 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

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

Voir 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()

Voir 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

Voir 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é

Voir 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é
Voir 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")
Voir 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)
Voir 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 ?

  1. Plus facile à lire pour les humains
  2. Opérations vectorisées plus rapides et meilleure utilisation du cache CPU
  3. Compatible avec Excel
  4. 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() ?

  1. read_csv est plus rapide
  2. scan_csv crée un LazyFrame et permet l’optimisation
  3. scan_csv ne supporte pas les gros fichiers
  4. Aucune différence
💡 Voir la réponse

Réponse : bscan_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 ?

  1. df["new"] = df["old"] * 2
  2. df.with_columns((pl.col("old") * 2).alias("new"))
  3. df.add_column("new", df["old"] * 2)
  4. 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” ?

  1. Supprime les colonnes inutiles
  2. Applique les filtres le plus tôt possible dans le pipeline
  3. Parallélise les calculs
  4. 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() ?

  1. Après chaque opération
  2. À la fin du pipeline Lazy pour déclencher l’exécution
  3. Pour convertir en Pandas
  4. 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 ?

  1. Ce n’est pas supporté
  2. C’est lent car ça passe par Python pour chaque ligne
  3. Ça modifie les données en place
  4. Ç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 ?

  1. CSV
  2. JSON
  3. Parquet
  4. 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 ?

  1. lf.show_plan()
  2. lf.explain(optimized=True)
  3. lf.describe()
  4. 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
Voir 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)")
Voir 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")
Voir 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

📖 Tutoriels & Articles

🔧 Outils complémentaires

  • DuckDB — SQL analytique ultra-rapide (compatible Polars)
  • PyArrow — Format Arrow sous-jacent

➡️ 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.

Voir 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é")
Retour au sommet