🐍 Python & traitement de donnĂ©es

Rappel — 🟱 junior : correct mais incomplet · đŸ”” confirmĂ© : le niveau attendu sur la plupart des postes · 🟣 senior : trade-offs, cas limites, ce qui casse Ă  l'Ă©chelle. RĂ©ponds d'abord Ă  voix haute, puis dĂ©plie. ← Tous les thĂšmes


6. pandas, Polars ou Spark : comment choisis-tu ?

trĂšs frĂ©quente · 🌍 international 🏩 local · EN — pandas, Polars or Spark — how do you choose?

🟱 RĂ©ponse junior

pandas pour les petits volumes, Spark quand les données sont trop grosses pour tenir en mémoire. Polars est une alternative plus rapide à pandas.

đŸ”” RĂ©ponse confirmĂ©

Je raisonne d'abord en ordre de grandeur, puis en coût opérationnel.

  • pandas : mono-thread, tout en mĂ©moire. Confortable jusqu'Ă  quelques Go, Ă  condition d'avoir environ 5 Ă  10× le volume du fichier en RAM (les copies intermĂ©diaires coĂ»tent cher). ÉcosystĂšme imbattable.
  • Polars : multi-thread, basĂ© sur Arrow, avec une API lazy qui optimise le plan avant exĂ©cution (projection et predicate pushdown). En mode streaming, il traite des volumes supĂ©rieurs Ă  la RAM. Sur une seule machine, souvent 5 Ă  20× plus rapide que pandas.
  • Spark : distribuĂ© sur plusieurs machines. La bonne rĂ©ponse Ă  partir de la dizaine de To, ou quand le cluster existe dĂ©jĂ .

Le critÚre décisif n'est pas seulement le volume : c'est le coût opérationnel. Spark apporte un cluster à administrer, une JVM, des temps de démarrage, et un débogage nettement plus pénible.

🟣 RĂ©ponse senior

Mon opinion, formĂ©e sur le terrain : la majoritĂ© des jobs Spark en production traitent moins de 10 Go et n'auraient jamais dĂ» ĂȘtre en Spark. On paie un cluster, une complexitĂ© de dĂ©ploiement et des heures de dĂ©bogage pour un traitement qu'une seule machine ferait en deux minutes avec DuckDB ou Polars. C'est le choix par rĂ©flexe le plus coĂ»teux du mĂ©tier.

Ce que je regarde vraiment, dans cet ordre :

1 — Le volume par exĂ©cution, pas le volume total. Une table de 5 To interrogĂ©e en incrĂ©mental sur une journĂ©e, c'est 8 Go par run. Le dimensionnement se fait sur le run, pas sur l'historique.

2 — L'existant. Si l'Ă©quipe opĂšre dĂ©jĂ  un cluster Spark, le coĂ»t marginal d'un job de plus est quasi nul, et l'homogĂ©nĂ©itĂ© a de la valeur. L'inverse — introduire Spark pour un seul pipeline — est trĂšs cher. Le meilleur outil est souvent celui que l'Ă©quipe sait dĂ©jĂ  exploiter Ă  3 h du matin.

3 — Le scale-up avant le scale-out. Une instance Ă  128 Go de RAM coĂ»te souvent moins cher qu'un cluster de cinq nƓuds, et supprime tout le shuffle rĂ©seau. C'est contre-intuitif aprĂšs dix ans de discours « distribuĂ© », mais depuis que DuckDB et Polars existent, la fenĂȘtre oĂč le mono-machine suffit s'est Ă©normĂ©ment Ă©largie.

4 — La nature du calcul. Certaines opĂ©rations (jointures massives entre deux trĂšs grandes tables, ML distribuĂ©) exigent rĂ©ellement du distribuĂ©. D'autres (filtres, agrĂ©gations, colonnes dĂ©rivĂ©es) passent trĂšs bien en mono-machine avec du Parquet partitionnĂ©.

5 — Le contexte Ă©conomique. Sur le marchĂ© local, oĂč l'on ne peut pas juste « prendre une plus grosse instance » et oĂč la facture cloud est scrutĂ©e, savoir faire tenir un traitement sur une machine modeste est une compĂ©tence diffĂ©renciante — pas une rĂ©gression.

En entretien, je réponds toujours par une question : quel volume par run, à quelle fréquence, avec quelle équipe pour maintenir ?

⚠ Le piĂšge — RĂ©pondre « Spark parce que c'est du Big Data » sans jamais parler du coĂ»t. Sur-dimensionner est aussi disqualifiant que sous-dimensionner.

📘 Pour approfondir : 17 · Polars · 11 · PySpark

🇬🇧 single-node vs distributed, lazy evaluation, out-of-core, scale-up vs scale-out, operational overhead


7. Comment traites-tu un fichier de 50 Go sur une machine qui a 8 Go de RAM ?

trĂšs frĂ©quente · 🌍 international 🏩 local · EN — How would you process a 50 GB file on a machine with 8 GB of RAM?

🟱 RĂ©ponse junior

Je lis le fichier par morceaux au lieu de tout charger d'un coup, par exemple avec chunksize dans pandas, et je traite chaque morceau l'un aprĂšs l'autre.

đŸ”” RĂ©ponse confirmĂ©

Plusieurs leviers combinés :

  • Lire en flux plutĂŽt qu'en bloc : pd.read_csv(..., chunksize=100_000), ou un gĂ©nĂ©rateur qui parcourt le fichier ligne Ă  ligne.
  • Ne lire que le nĂ©cessaire : sĂ©lectionner les colonnes utiles (usecols) et forcer des types compacts (category plutĂŽt que object, int32 plutĂŽt que int64) — souvent 50 Ă  80 % de mĂ©moire en moins.
  • Changer de format : convertir le CSV en Parquet (colonne, compressĂ©). On ne lit alors que les colonnes nĂ©cessaires et les filtres descendent au niveau des fichiers (predicate pushdown).
  • Utiliser un moteur out-of-core : DuckDB ou Polars en mode streaming traitent des volumes supĂ©rieurs Ă  la RAM en Ă©crivant sur disque quand nĂ©cessaire.
import duckdb
duckdb.sql("""
    SELECT country, SUM(amount) AS total
    FROM read_csv_auto('transactions.csv')
    GROUP BY country
""").write_parquet('agg.parquet')
🟣 RĂ©ponse senior

Ma premiĂšre question : pourquoi est-ce un CSV de 50 Go ? Si je peux agir en amont, convertir en Parquet partitionnĂ© rĂšgle le problĂšme plus sĂ»rement que n'importe quelle astuce de lecture — typiquement 5 Ă  10× plus petit, et surtout on cesse de lire ce dont on n'a pas besoin. Beaucoup de « problĂšmes de volumĂ©trie » sont en rĂ©alitĂ© des problĂšmes de format.

Ensuite, tout dépend de l'opération, et c'est ce que les candidats manquent :

  • Filtre ou projection : trivial en streaming, la mĂ©moire ne croĂźt pas.
  • AgrĂ©gation : possible en une passe si l'agrĂ©gat est additif (somme, min, max, count) — on cumule par morceau puis on combine. Mais attention aux agrĂ©gats non additifs : une mĂ©diane ou un COUNT(DISTINCT) exacts ne se combinent pas ainsi. Il faut soit un passage supplĂ©mentaire, soit une structure approximative (HyperLogLog pour le distinct, t-digest pour les quantiles) — et alors on assume une marge d'erreur, ce qui est une dĂ©cision mĂ©tier Ă  faire valider.
  • Tri global ou jointure : c'est le cas rĂ©ellement difficile. On passe par un partitionnement par hash sur disque (tri externe / grace hash join), et lĂ  je laisse DuckDB ou Polars faire — rĂ©implĂ©menter ça Ă  la main est une mauvaise idĂ©e.

Le point souvent oublié : la RAM n'est pas la seule contrainte. Il faut de l'espace disque temporaire pour le débordement, et le disque devient le goulot d'étranglement (un SSD NVMe change tout par rapport à un disque réseau).

Enfin, l'arbitrage Ă©conomique. Sur un cloud, la rĂ©ponse honnĂȘte est parfois « je prends une instance Ă  64 Go pendant vingt minutes » : quelques euros contre une journĂ©e d'ingĂ©nierie. Mais dans un contexte oĂč l'infrastructure est fixe — beaucoup d'entreprises locales, on-prem —, savoir faire tenir le traitement dans 8 Go est une vraie compĂ©tence. Je prĂ©cise toujours ce contexte avant de choisir.

⚠ Le piĂšge — RĂ©pondre « j'utilise Spark ». Sur une seule machine avec 8 Go, Spark n'apporte rien et ajoute une JVM. Le recruteur teste si tu comprends la diffĂ©rence entre parallĂ©lisme et traitement out-of-core.

📘 Pour approfondir : 05 · Python data processing · 18 · High performance Python

🇬🇧 out-of-core, streaming, chunking, predicate pushdown, spill to disk, approximate aggregation


8. Comment tu testes un pipeline de données ?

frĂ©quente · 🌍 international · EN — How do you test a data pipeline?

🟱 RĂ©ponse junior

Je lance le pipeline sur un petit jeu de données et je vérifie que le résultat est correct, en regardant quelques lignes en sortie.

C'est un test manuel : il ne protĂšge de rien la prochaine fois.

đŸ”” RĂ©ponse confirmĂ©

Je distingue trois niveaux, complémentaires :

  1. Tests unitaires sur la logique de transformation : je sépare les entrées/sorties du calcul, et je teste des fonctions pures avec de petits DataFrames écrits à la main. Rapides, exécutés à chaque commit.
  2. Tests d'intégration : le pipeline complet sur un jeu de données réduit mais réaliste, avec les vraies dépendances (base de test, stockage local type MinIO).
  3. Tests de donnĂ©es en production : ce ne sont pas les mĂȘmes ! Le code peut ĂȘtre juste et la donnĂ©e fausse. Avec dbt (unique, not_null, accepted_values, relationships) ou Great Expectations, je valide le rĂ©sultat Ă  chaque exĂ©cution.

Le tout branché en CI : si un test échoue, la pull request est bloquée.

🟣 RĂ©ponse senior

Le prĂ©alable, c'est de rendre le code testable. Un pipeline oĂč lecture, transformation et Ă©criture sont entremĂȘlĂ©es dans une seule fonction de 200 lignes n'est pas testable — et aucun framework ne rattrapera ça. J'isole systĂ©matiquement la transformation en fonctions pures (DataFrame → DataFrame) : le reste devient de la plomberie.

Ensuite, ce que je teste en prioritĂ©, ce ne sont pas les cas nominaux — ils marchent presque toujours — mais les cas limites : fichier vide, colonne manquante ou renommĂ©e par la source, type inattendu (un montant qui arrive en texte avec une virgule dĂ©cimale), doublon, donnĂ©e en retard, caractĂšres non-ASCII, fuseau horaire. C'est lĂ  que les pipelines cassent rĂ©ellement.

Deux tests que je considÚre non négociables et que je vois rarement :

  • L'idempotence, testĂ©e explicitement : exĂ©cuter deux fois le mĂȘme run doit produire exactement le mĂȘme Ă©tat final. C'est le socle de tout rejeu et de tout backfill.
  • La non-rĂ©gression sur incidents passĂ©s : chaque bug de production devient un cas de test permanent. C'est ainsi qu'une suite de tests gagne en valeur avec le temps au lieu d'ĂȘtre un rituel.

Sur les fixtures, je prĂ©fĂšre quelques dizaines de lignes Ă©crites Ă  la main et lisibles Ă  un dump de production de 100 Mo : quand un test Ă©choue, on doit comprendre pourquoi en dix secondes. Et jamais de donnĂ©es rĂ©elles de clients dans un dĂ©pĂŽt — sujet RGPD, mais aussi bon sens.

Enfin, la limite Ă  connaĂźtre et Ă  Ă©noncer : 100 % de couverture unitaire ne dit rien sur la qualitĂ© de la donnĂ©e en production. Le code peut ĂȘtre parfait et la source envoyer des montants en centimes depuis mardi. D'oĂč la nĂ©cessitĂ© des deux couches — tests de code en CI, tests de donnĂ©es Ă  l'exĂ©cution — plus une surveillance de fraĂźcheur : une table qui ne se met plus Ă  jour est un incident, mĂȘme si tous les tests passent.

⚠ Le piĂšge — Ne parler que de tests unitaires. En data, un code juste peut produire une donnĂ©e fausse : si tu ne distingues pas tests de code et tests de donnĂ©es, tu montres que tu n'as pas opĂ©rĂ© de pipeline en production.

📘 Pour approfondir : 25 · dbt & Data Quality · 03 · Git

🇬🇧 unit test, integration test, data test, fixture, idempotency, regression test, freshness check


9. Pourquoi iterrows() est-il déconseillé ? Que fais-tu à la place ?

frĂ©quente · 🌍 international 🏩 local · EN — Why is iterrows() discouraged in pandas, and what do you use instead?

🟱 RĂ©ponse junior

Parce que c'est lent : ça boucle ligne par ligne en Python. Il vaut mieux vectoriser, c'est-à-dire appliquer l'opération sur toute la colonne d'un coup.

đŸ”” RĂ©ponse confirmĂ©

iterrows() fait deux choses coĂ»teuses : il exĂ©cute une boucle Python (donc interprĂ©tĂ©e, avec le GIL) et il crĂ©e une Series par ligne, ce qui alloue de la mĂ©moire Ă  chaque itĂ©ration — et fait au passage perdre les types (tout remonte en object si les colonnes sont hĂ©tĂ©rogĂšnes).

À la place, on utilise des opĂ©rations vectorisĂ©es, exĂ©cutĂ©es en C sur des tableaux contigus :

# ❌ lent
for idx, row in df.iterrows():
    df.at[idx, 'ttc'] = row['ht'] * 1.18

# ✅ vectorisĂ©
df['ttc'] = df['ht'] * 1.18

# ✅ conditionnel vectorisĂ©
df['segment'] = np.where(df['montant'] > 1000, 'premium', 'standard')

L'écart est typiquement de deux à trois ordres de grandeur. Autre réflexe : quand on boucle pour aller chercher une valeur dans une autre table, la bonne réponse est un merge, pas une boucle. Et apply n'est pas vectorisé : c'est une boucle déguisée, un peu plus lisible seulement.

🟣 RĂ©ponse senior

La hiérarchie réelle, du plus rapide au plus lent : opération vectorisée native > NumPy > itertuples() > apply() > iterrows(). Si je dois vraiment itérer, itertuples() est nettement meilleur (namedtuples, types préservés).

Mais voici ce que je rĂ©ponds surtout en entretien, parce que c'est le vrai enseignement : en data engineering, le goulot d'Ă©tranglement est rarement le CPU. Il est dans l'I/O, la sĂ©rialisation et le rĂ©seau. Optimiser une boucle sur 10 000 lignes pendant que le job lit 200 Go de CSV non partitionnĂ©, c'est optimiser au mauvais endroit — un gain de 3 secondes sur un traitement de 40 minutes. Je profile avant d'optimiser (cProfile, %%timeit, l'onglet Stages du Spark UI) ; l'intuition se trompe trĂšs souvent.

Il y a aussi des cas oĂč la boucle est lĂ©gitime : un volume faible et une logique sĂ©quentielle irrĂ©ductible (une machine Ă  Ă©tats, un calcul dĂ©pendant de la ligne prĂ©cĂ©dente qu'on ne peut pas exprimer en fenĂȘtrage). Écrire du code vectorisĂ© illisible pour gagner 200 ms sur un DataFrame de 500 lignes est une mauvaise dĂ©cision d'ingĂ©nierie — la lisibilitĂ© a une valeur.

L'Ă©quivalent en Spark mĂ©rite d'ĂȘtre mentionnĂ©, car c'est le mĂȘme piĂšge Ă  plus grande Ă©chelle : une UDF Python force la sĂ©rialisation de chaque ligne entre la JVM et un interprĂ©teur Python, et rend le traitement opaque Ă  l'optimiseur Catalyst (qui ne peut plus pousser de filtres). Une fonction native est souvent 10 Ă  100× plus rapide. Si une UDF est inĂ©vitable, on utilise une pandas UDF (vectorisĂ©e, via Arrow) pour limiter les dĂ©gĂąts.

⚠ Le piĂšge — Dire « apply est vectorisĂ© ». C'est faux, et c'est une erreur qui se repĂšre immĂ©diatement. apply boucle, il est juste plus lisible.

📘 Pour approfondir : 18 · High performance Python · 19 · PySpark avancĂ©

🇬🇧 vectorization, GIL, UDF overhead, serialization, pandas UDF, Arrow, profiling


10. Comment structures-tu un projet Python de data engineering ?

frĂ©quente · 🌍 international · EN — How do you structure a production-grade Python data engineering project?

🟱 RĂ©ponse junior

Je mets mes scripts dans un dossier, avec un requirements.txt pour les dépendances et un README pour expliquer comment lancer.

đŸ”” RĂ©ponse confirmĂ©

Une structure type :

projet/
├── pyproject.toml          # dĂ©pendances + mĂ©tadonnĂ©es (uv / poetry)
├── Dockerfile
├── README.md
├── src/pipeline/
│   ├── extract.py          # I/O en entrĂ©e
│   ├── transform.py        # logique mĂ©tier — pure, testable
│   ├── load.py             # I/O en sortie
│   └── config.py           # configuration via variables d'environnement
├── tests/
└── dags/                   # orchestration (Airflow)

Les principes : configuration externalisée (jamais de chemin ni de mot de passe en dur), séparation nette entre logique métier et I/O pour pouvoir tester, dépendances verrouillées par un lockfile, conteneurisation pour un environnement reproductible, et qualité automatisée en CI (ruff, black, mypy, pytest).

🟣 RĂ©ponse senior

La structure doit suivre le mode de dĂ©ploiement, pas un modĂšle recopiĂ© d'un article de blog. Un job packagĂ© en image Docker et lancĂ© par Airflow n'a pas la mĂȘme arborescence qu'une bibliothĂšque partagĂ©e entre quinze pipelines.

Les points sur lesquels je ne transige pas :

Le DAG ne contient pas de logique. Un fichier Airflow doit dĂ©crire un enchaĂźnement, appeler des fonctions et rien d'autre. DĂšs qu'on met des transformations dans le DAG, on ne peut plus tester sans Airflow — et le scheduler parse ces fichiers en boucle, donc tout import lourd dĂ©grade tout l'ordonnanceur.

Un lockfile, sinon le build n'est pas reproductible. Un requirements.txt sans versions figées signifie qu'une mise à jour transitive peut casser la production un mardi matin sans qu'aucun commit n'ait été fait. C'est l'incident le plus stupide et le plus fréquent.

Monorepo ou un dĂ©pĂŽt par pipeline ? Le monorepo facilite le partage de code et le refactoring transverse, mais impose une CI plus fine (ne rebuilder que ce qui change) et couple les cycles de livraison. Un dĂ©pĂŽt par pipeline isole bien mais duplique la plomberie et laisse dĂ©river les versions. À moins de cinq pipelines, monorepo sans hĂ©siter.

Les secrets ne sont jamais dans le code, ni dans un .env commité, ni dans une variable Airflow en clair : un gestionnaire de secrets, ou à défaut des variables d'environnement injectées au déploiement. Et une rotation possible sans redéployer.

Un dĂ©tail spĂ©cifique au mĂ©tier : le packaging pour Spark. Envoyer ses dĂ©pendances avec --py-files fonctionne mal dĂšs qu'il y a des paquets compilĂ©s ; construire une image contenant l'environnement est plus fiable, au prix d'une image plus lourde (d'oĂč le multi-stage build).

Enfin, l'arbitrage contextuel : une Ă©quipe d'une personne n'a pas besoin de la mĂȘme industrialisation qu'une Ă©quipe de dix. Imposer un monorepo, mypy strict et une CI en quatre Ă©tapes Ă  un DE seul dans une PME, c'est de la cĂ©rĂ©monie qui ralentit sans protĂ©ger. Je calibre selon l'Ă©quipe et la criticitĂ© — et je sais expliquer pourquoi.

⚠ Le piĂšge — DĂ©crire une arborescence sans jamais expliquer pourquoi. La question teste ta maturitĂ© d'ingĂ©nieur logiciel : reproductibilitĂ©, testabilitĂ©, sĂ©curitĂ©.

📘 Pour approfondir : 14 · Docker · 12 · Orchestration

🇬🇧 lockfile, reproducible build, separation of concerns, monorepo, secrets management, multi-stage build


← ThĂšme prĂ©cĂ©dent : đŸ—„ïž SQL & modĂ©lisation

📋 Tous les thùmes

ThĂšme suivant : ⚡ Spark & calcul distribuĂ© →