Quand pandas ne suffit plus
pandas charge toutes les données dans la mémoire d’une seule machine. Au-delà de quelques dizaines de gigaoctets, ou quand un traitement doit lire des milliers de fichiers d’un lac de données, il faut répartir le travail. Apache Spark est le moteur de calcul distribué le plus répandu pour cela : il découpe les données en partitions et les traite en parallèle sur plusieurs machines, en mémoire autant que possible. On l’utilise en Python avec PySpark. Ce cours s’appuie sur l’API DataFrame de Spark 3.5 et 4.x.
Spark n’est pas toujours la bonne réponse : pour quelques millions de lignes, pandas ou DuckDB sur une machine sont plus simples et souvent plus rapides. Spark devient pertinent quand le volume ou le parallélisme l’exige.
L’architecture en trois rôles
- Le driver exécute votre script, construit le plan de calcul et distribue le travail.
- Les executors sont des processus lancés sur les machines du cluster ; chacun traite des tâches, une tâche correspondant à une partition de données.
- Le gestionnaire de cluster (Kubernetes, YARN, le mode autonome de Spark, ou un service managé) attribue les ressources.
En local, tout tourne dans un seul processus (master("local[*]") utilise tous les cœurs) : pratique pour apprendre, avec exactement la même API.
Créer une session et lire des données
from pyspark.sql import SparkSession
from pyspark.sql import functions as F
spark = (SparkSession.builder
.appName("ventes")
.master("local[*]") # en cluster, ce paramètre est fourni au lancement
.getOrCreate())
schema = "id INT, jour DATE, region STRING, produit STRING, montant DOUBLE"
ventes = (spark.read
.option("header", True)
.option("mode", "DROPMALFORMED") # ignorer les lignes illisibles
.schema(schema)
.csv("s3a://lake/brut/ventes/")) # un dossier entier : tous ses fichiers
ventes.printSchema()
ventes.show(5)
Le schéma explicite évite deux problèmes de inferSchema=True : une lecture supplémentaire des données pour deviner les types, et des types devinés faux (un code postal lu comme entier perd son zéro initial). Le mode de lecture décide du sort des lignes mal formées : PERMISSIVE (par défaut) les garde en mettant à null ce qui ne se lit pas, DROPMALFORMED les écarte, FAILFAST arrête la lecture à la première erreur.
Pour les données déjà préparées, préférez Parquet : format en colonnes, compressé, qui embarque son schéma (spark.read.parquet(...)).
Transformer un DataFrame
L’API ressemble à pandas, mais avec un style déclaratif inspiré de SQL :
resultat = (ventes
.filter(F.col("montant") > 0)
.withColumn("ttc", F.round(F.col("montant") * 1.2, 2))
.withColumn("gamme", F.when(F.col("montant") >= 200, "haut").otherwise("standard"))
.select("jour", "region", "produit", "ttc", "gamme"))
Trois règles :
- Un DataFrame est immuable :
withColumnne modifie pasventes, il renvoie un nouveau DataFrame. Il faut donc réaffecter ou enchaîner. - Les expressions sur colonnes (
F.col,F.when,F.to_date,F.upper…) sont exécutées dans le moteur de Spark, optimisées et distribuées. Une fonction Python appliquée ligne à ligne (UDF) est bien plus lente : réservez-la aux cas sans équivalent natif. - Rien n’est encore calculé à ce stade : Spark attend une action (
show,count,write…), comme on le verra à la leçon suivante.
Ramener des données vers le driver
show(n) affiche quelques lignes ; count() compte ; collect() rapatrie toutes les lignes dans la mémoire du driver. Sur un vrai volume, collect() fait tomber le driver : préférez limit(n), show(), ou écrivez le résultat (write.parquet). Pour passer à pandas un résultat déjà agrégé et petit, toPandas() convient.
À retenir
- Driver = chef d’orchestre, executors = ouvriers, une tâche par partition.
- Schéma explicite et mode de lecture choisi ; Parquet pour les données préparées.
- DataFrame immuable, fonctions F plutôt que Python ligne à ligne, et jamais de
collect()sur un gros volume.
Exercices
Les solutions et les tests restent sur le serveur. Dans l'espace étudiant, ton code est exécuté et corrigé, et le tuteur donne des indices avant la solution.
Pipeline en Python pur
Reproduisez en Python pur ce pipeline Spark appliqué à une liste de dictionnaires :
(df.filter(F.col("montant") > 0) .withColumn("ttc", F.round(F.col("montant") * 1.2, 2)) .withColumn("gamme", F.when(F.col("montant") >= 200, "haut").otherwise("standard")) .select("region", "ttc", "gamme"))Écrivez
pipeline(lignes)qui renvoie une nouvelle liste de dictionnaires avec uniquement les clésregion,ttc,gamme, sans modifierlignes.def pipeline(lignes): ...Modes de lecture d'un CSV
Simulez la lecture d’un CSV sans en-tête avec un schéma.
schemaest une liste de couples(nom, type)oùtypevautint,floatoustr. Écrivezlire(lignes, schema, mode="PERMISSIVE"):- chaque ligne est découpée sur
,et chaque valeur convertie avec le type ; - une ligne est mal formée si son nombre de champs diffère du schéma ou si une conversion échoue ;
PERMISSIVE: la ligne est gardée, les valeurs illisibles ou manquantes valentNone(les champs en trop sont ignorés) ;DROPMALFORMED: la ligne est écartée ;FAILFAST: levezValueError.
Renvoyez une liste de dictionnaires.
def lire(lignes, schema, mode="PERMISSIVE"): ...- chaque ligne est découpée sur
Pourquoi un schéma explicite ?
Quel est l’intérêt principal de
.schema(...)par rapport àinferSchema=Truesur un gros volume ?- Il compresse les données
- Il évite une lecture supplémentaire des données et garantit les types attendus
- Il permet de lire des fichiers Excel
- Il active la mise en cache
Quiz de fin de leçon
Qui traite les partitions de données dans un cluster Spark ?
- Le driver
- Les executors
- Le gestionnaire de cluster
Que fait
df.withColumn("x", F.lit(1))surdf?- Il ajoute la colonne à df
- Il renvoie un nouveau DataFrame ; df est inchangé
- Il lève une erreur
Pourquoi éviter
collect()sur un DataFrame de 500 millions de lignes ?- Parce que collect est déprécié
- Parce que toutes les lignes sont rapatriées dans la mémoire du driver
- Parce qu'il supprime les données
La suite du cours
- Transformations, actions et évaluation paresseuse20 min
- Partitions, shuffle et jointures20 min
- Spark SQL et fonctions de fenêtre20 min
- Optimiser un job Spark20 min