Python et Pandas

30 questions en accès libre

Pandas, NumPy, nettoyage, types et performance : les questions Python d’un entretien data analyst ou data scientist.

Niveau

Cliquez sur une question pour dérouler la réponse attendue.

  1. 01Quelle différence faites-vous entre une Series et un DataFrame ?Juniorpandas

    Une Series est une colonne étiquetée : une dimension, un index, un nom, un dtype. Un DataFrame est une table : deux dimensions, un index de lignes, un index de colonnes, et autant de dtypes que de colonnes.

    La phrase à poser en entretien : le DataFrame n’est pas « plusieurs listes », c’est un alignement de Series sur le même index. C’est cet alignement qui fait le succès et les pièges de Pandas.

    python
    df["montant"]          # Series — une colonne
    df[["montant"]]        # DataFrame — une table d’une colonne
    df.loc[3]              # Series — une ligne, index = noms de colonnes

    Les crochets simples ou doubles changent le type de retour, donc les méthodes disponibles. df["montant"].mean() est une réduction ; df[["montant"]].mean() renvoie une Series indexée par le nom de colonne. Beaucoup de bugs de début de poste viennent de là : on enchaîne une méthode de Series sur un objet qui est encore un DataFrame.

    Deux précisions qui montrent que vous avez ouvert un notebook :

    • L’index se propage. Additionner deux Series aligne les étiquettes, pas les positions. Des identifiants décalés produisent des NaN, pas une erreur.
    • Une Series a un name. Après un groupby, ce nom devient souvent le nom de colonne : le perdre rend le prochain merge illisible.

    Vous n’avez pas à réciter l’API. Vous avez à dire que vous choisissez Series pour une grandeur, DataFrame dès qu’il y a plusieurs grandeurs à garder alignées.

  2. 02Quand utilisez-vous loc plutôt que iloc ?Juniorpandas

    loc sélectionne par étiquette. iloc sélectionne par position entière, comme une liste Python. Vous utilisez loc dès que l’index a un sens métier — identifiant client, date, code — et iloc seulement quand la place dans le tableau compte, par exemple les cent premières lignes d’un échantillon.

    Le piège que l’intervieweur attend :

    python
    s = pd.Series([10, 20, 30], index=[2, 3, 4])
    s.loc[2:3]    # étiquettes 2 et 3 → 10, 20  (borne finale incluse)
    s.iloc[2:3]   # positions 2 à 3 exclue → 30

    loc inclut la borne de droite. iloc la exclut, convention Python. Répondre « l’un c’est les noms, l’autre c’est les numéros » sans ce détail est une réponse incomplète.

    loc accepte aussi un masque booléen, et c’est son usage le plus fréquent en nettoyage :

    python
    df.loc[df["montant"] < 0, "montant"] = 0

    Une seule opération, index et colonne nommés. C’est aussi la forme qui évite le SettingWithCopyWarning.

    L’anti-réflexe : df[0:5] ou df[df.a > 0]["b"]. Le premier est de l’iloc déguisé sur les lignes ; le second enchaîne deux indexations et vous expose à une copie silencieuse. En entretien, vous dites : sélectionner et assigner avec loc en une fois.

  3. 03Que signifie SettingWithCopyWarning, et comment le faites-vous disparaître ?Intermédiairepandas

    Pandas ne sait pas si vous écrivez dans une vue ou dans une copie ; l’affectation peut ne rien changer. On assigne en une seule fois avec loc, sans indexation chaînée.

    Lire la réponse détaillée
  4. 04Comment savez-vous si vous tenez une vue ou une copie ?Seniorpandas

    Une vue partage la mémoire du tableau d’origine, une copie non. Pandas n’a longtemps pas garanti laquelle vous teniez : on copie quand on veut muter, et le copy-on-write referme l’ambiguïté.

    Lire la réponse détaillée
  5. 05Comment les dtypes déterminent-ils la mémoire d’un DataFrame ?Intermédiairepandas

    Un type trop large ou une colonne object de chaînes Python gonfle la RAM. On fixe les dtypes à la lecture, on downcast, et on passe en category les colonnes à faible cardinalité.

    Lire la réponse détaillée
  6. 06Comment décidez-vous entre dropna et fillna ?Juniorpandas

    Ni dropna ni fillna n’est une politique. La question précède la méthode : pourquoi cette cellule est-elle vide ?

    Trois décisions, pas deux :

    • Laisser vide quand le vide informe — une date de résiliation absente signifie « encore client », pas « à imputer ».
    • dropna quand la ligne (ou la colonne) n’est pas utilisable et que la perdre ne biaise pas la question que vous posez. Une ligne sur dix mille, clé absente : on peut jeter. 40 % d’une colonne cible : on ne jette pas sans le dire au métier.
    • fillna seulement quand vous inventez une valeur défendable : médiane d’un délai, modalité "inconnu", zéro si zéro est le vrai sens métier (un stock, pas un revenu).
    python
    df["canal"] = df["canal"].fillna("inconnu")
    df = df.dropna(subset=["commande_id"])

    Toujours nommer les colonnes. df.dropna() sans subset détruit des lignes pour un champ optionnel que personne n’utilise. fillna(0) sur tout le tableau mélange des grandeurs où zéro est légitime et d’autres où il est un mensonge.

    Ce que l’intervieweur veut entendre : vous comptez d’abord (df.isna().mean()), vous séparez les colonnes, et vous documentez le choix. Le détail des cas est dans nettoyer des données sales.

    Le piège junior : remplir pour que « ça passe » dans un calcul. Un mean() ignore déjà les NA. Remplir à zéro avant la moyenne tire le résultat vers zéro sans que personne ne le voie.

  7. 07Quand convertissez-vous une colonne en type catégoriel ?Intermédiairepandas

    Quand la cardinalité est basse devant le nombre de lignes, et que les valeurs sont un ensemble fermé — ou que vous acceptez de le fermer. Pays, canal, statut, tranche d’âge : astype("category") remplace des millions de chaînes par un entier de code plus une table de modalités. Gain de mémoire, groupby plus honnête, ordre possible.

    python
    df["statut"] = df["statut"].astype(
        pd.CategoricalDtype(["brouillon", "actif", "clos"], ordered=True)
    )

    ordered=True n’est pas décoratif : brouillon < actif < clos devient un ordre que min, max et un tri comprennent. Sans ordre, comparer des catégories est une erreur, et c’est tant mieux.

    Deux pièges à citer.

    Une valeur nouvelle devient NA. Le dtype catégoriel rejette ce qui n’est pas dans les catégories. Un export qui ajoute "suspendu" silencieuse vos lignes. En pipeline, vous décidez : étendre les catégories, ou échouer.

    python
    df["statut"] = df["statut"].cat.add_categories(["suspendu"])

    groupby et les catégories inutilisées. Par défaut, Pandas peut encore aligner toutes les modalités, y compris celles à zéro ligne. groupby("statut", observed=True) ne garde que ce qui apparaît. Oublier observed gonfle un tableau de contingence et surprend un merge.

    Ce que vous ne catégorisez pas : un identifiant quasi unique, un commentaire libre, un timestamp. La table des modalités serait plus grosse que la colonne. Vérifier df["col"].nunique() avant le astype, pas après le MemoryError.

  8. 08Comment gérez-vous le parse de dates, les fuseaux et le rééchantillonnage ?Intermédiairepandas

    Trois opérations distinctes, que les candidats mélangent.

    Parser. Vous ne laissez pas Pandas deviner. Un 03/04/2026 n’a pas le même mois à Paris et à New York.

    python
    df["inscrit_le"] = pd.to_datetime(
        df["inscrit_le"],
        format="%d/%m/%Y",
        errors="coerce",
    )

    errors="coerce" transforme l’impardonnable en NaT, donc comptable. Si 30 % deviennent NaT, le problème est la source. format="mixed" et dayfirst=True existent pour les exports hétérogènes ; ce n’est pas un permis de parser « au feeling ». Le tutoriel nettoyer des données sales insiste là-dessus.

    Fuseau. Une colonne naive n’est pas « UTC par défaut », c’est une colonne sans contrat.

    python
    s = s.dt.tz_localize("America/Toronto")   # poser le fuseau d’origine
    s = s.dt.tz_convert("UTC")                # convertir pour agréger

    tz_localize pose, tz_convert change. Les inverser lève une erreur — ou, pire, vous localisez deux fois. Pour un merge de deux sources, vous vous mettez d’accord sur UTC avant de joindre, sinon minuit Toronto et minuit UTC se croisent.

    Rééchantillonner. resample exige un index datetime (idéalement monotone). Vous triez, vous indexez, puis vous choisissez une agrégation métier : somme de montant, dernier statut, nombre de événements — pas « mean partout ».

    python
    (
        df.sort_values("inscrit_le")
          .set_index("inscrit_le")
          .resample("W")["montant"]
          .sum()
    )

    rolling n’est pas resample : l’un glisse à chaque ligne, l’autre aligne sur une grille de temps. Les confondre en entretien suffit à relancer sur les fenêtres.

  9. 09Comment choisissez-vous le how et les clés d’un merge ?Intermédiairepandas

    how n’est pas une préférence de syntaxe. C’est qui a le droit de disparaître.

    howQui survit
    innerseulement les clés des deux côtés
    lefttoutes les lignes de gauche, droite facultative
    rightl’inverse
    outerl’union des clés
    crossle produit cartésien, sans clé

    Vous partez du grain métier. Une table de commandes à enrichir avec le client : left sur commandes, clé client_id. Un rapprochement « uniquement les matchs » pour un contrôle : inner, puis vous comptez les exclus. outer pour un audit d’écarts, pas pour un tableau de bord.

    Les clés : on="client_id" si le nom est partagé ; left_on / right_on sinon. Joindre sur l’index, c’est join, qui n’est qu’un merge avec left_index / right_index. Ce qui compte, ce n’est pas le nom de la méthode, c’est l’unicité des clés.

    python
    out = commandes.merge(
        clients,
        on="client_id",
        how="left",
        validate="m:1",
        indicator=True,
    )

    validate="m:1" fait échouer un many-to-many accidentel — la relance classique de cette question. indicator=True produit _merge : both, left_only, right_only. Vous affichez out["_merge"].value_counts() avant de jeter l’indicateur.

    Pièges à énoncer : types de clé différents ("001" contre 1), NA dans la clé (les NA ne matchent pas les NA), suffixes _x / _y qui masquent une collision de colonnes. Vous alignez le dtype de la clé avant le merge, vous ne « corrigez » pas après coup en filtrant des lignes inexplicablement dupliquées.

  10. 10Que recouvre split-apply-combine dans un groupby ?Intermédiairepandas

    Trois temps, un seul objet.

    1. Split — Pandas découpe les lignes selon les clés (["pays", "canal"]).
    2. Apply — il applique une opération à chaque groupe : une agrégation, une transformation alignée, ou un filtre.
    3. Combine — il réassemble. Le forme du résultat dépend de l’opération, pas du groupby lui-même.

    C’est la distinction que l’intervieweur attend, parce que les trois verbes ne renvoient pas le même grain.

    python
    g = df.groupby("pays", observed=True)
    
    g["montant"].sum()                         # agrégation : une ligne par pays
    g["montant"].transform("sum")              # même longueur que df, utile pour une part
    g.filter(lambda x: x["montant"].sum() > 0) # sous-DataFrame : groupes entiers gardés ou non

    agg réduit. transform recycle le résultat sur chaque ligne du groupe — classic pour une part du total, un z-score intra-groupe, un rang. filter garde ou jette des groupes, pas des cellules. Utiliser apply pour ces trois cas est possible et presque toujours plus lent, plus opaque.

    Agrégations nommées, pour un compte rendu lisible :

    python
    df.groupby("pays", as_index=False).agg(
        revenu=("montant", "sum"),
        n=("commande_id", "nunique"),
    )

    as_index=False évite un MultiIndex dont vous n’avez pas besoin. observed=True est obligatoire dès qu’une clé est category, sinon les modalités fantômes réapparaissent.

    La phrase de maîtrise : vous choisissez le verbe selon le grain que vous voulez retrouver, pas selon « ce qui marche dans le notebook ».

  11. 11Pourquoi apply est-il lent, et par quoi le remplacez-vous ?Intermédiairepandas

    apply exécute du Python ligne à ligne. On vectorise avec les opérations natives, NumPy ou les accesseurs str et dt ; apply ne reste que pour une logique vraiment irrégulière.

    Lire la réponse détaillée
  12. 12Quand choisissez-vous map, replace ou np.where ?Juniorpandas

    Trois outils, trois intentions.

    map recode une Series via un dictionnaire (ou une fonction). Les clés absentes deviennent NA — c’est le piège, et c’est parfois le contrat voulu.

    python
    df["pays_nom"] = df["pays"].map({"CA": "Canada", "FR": "France"})

    Un code "US" non listé sort NA. Si vous vouliez garder l’original, ce n’est pas map.

    replace substitue des valeurs, sur une Series ou un DataFrame, et laisse intact ce qu’il ne connaît pas.

    python
    df["pays"] = df["pays"].replace({"USA": "US", "États-Unis": "US"})

    Idéal pour normaliser des synonymes. replace accepte aussi une regex ; n’en abusez pas sur une colonne numérique.

    np.where / np.select expriment une condition vectorisée, pas une table de correspondance.

    python
    df["alerte"] = np.where(df["montant"] < 0, "negatif", "ok")

    Dès qu’il y a plus de deux branches, np.select reste lisible ; une cascade de np.where imbriqués ne l’est pas.

    En une phrase : table de recodage fermée → map ; correction de valeurs, le reste inchangé → replace ; règle booléenne → where. Vous ne faites pas un apply de if pour ces trois cas.

  13. 13Quelle différence entre une table pivot et melt ?Intermédiairepandas

    Ce sont les deux sens du même trajet : long ↔ large.

    pivot_table (et pivot) étale : une dimension de lignes devient des colonnes. melt empile : des colonnes du même genre redeviennent une paire (nom, valeur).

    python
    large = df.pivot_table(
        index="mois",
        columns="canal",
        values="montant",
        aggfunc="sum",
        fill_value=0,
    )
    
    long = large.reset_index().melt(
        id_vars="mois",
        var_name="canal",
        value_name="montant",
    )

    La différence qui compte avec pivot : pivot_table agrège. Deux lignes le même mois, même canal → sum (ou mean, count). pivot exige l’unicité du couple index × colonnes et lève une erreur sinon. En entretien, vous dites lequel vous prenez parce que vous savez si le grain est déjà unique.

    melt a besoin qu’on nomme ce qui reste identifiant (id_vars) et ce qui est une mesure étalée. Oublier un identifiant, c’est mélanger des grandeurs.

    stack / unstack font le même travail sur un MultiIndex : plus naturels après un groupby à plusieurs clés. pivot_table est plus parlant quand les colonnes s’appellent encore « canal », « mois ».

    Usage typique : un export Excel « une colonne par mois » arrive en large ; presque tout calcul Pandas (groupby, merge, rolling) est plus simple en long. Vous melt d’abord, vous pivotez à la fin pour un tableau de lecture humaine.

  14. 14Quand un MultiIndex est-il utile, et comment le ramenez-vous à plat ?Intermédiairepandas

    Un MultiIndex est utile quand la hiérarchie sert à sélectionner, pas quand elle est un accident de groupby. Deux niveaux paysville, un pivot_table mois × canal, un stack : vous voulez df.xs("CA", level="pays") ou df.loc[("CA", "Montréal")]. Là, le garder.

    Dès que vous exportez, joignez, ou passez l’objet à un outil qui attend des colonnes plates, vous le démontez. La majorité des notebooks de production n’ont pas besoin d’un index à trois niveaux ; ils en héritent.

    python
    s = df.groupby(["pays", "canal"])["montant"].sum()
    plat = s.reset_index()                 # les clés redeviennent des colonnes
    plat.columns = ["pays", "canal", "montant"]

    Après un pivot_table, les colonnes elles-mêmes peuvent être un MultiIndex (values × columns). reset_index ne suffit pas :

    python
    large.columns = [
        "_".join(map(str, col)).strip("_") for col in large.columns.to_flat_index()
    ]
    large = large.reset_index()

    Ou, plus propre : large.droplevel(0, axis=1) si un niveau est redondant, puis reset_index().

    Piège : as_index=False dès le groupby évite de créer l’objet que vous alliez détruire. Créer un MultiIndex pour le reset_index à la ligne suivante n’est pas une architecture, c’est une habitude.

    Vous le gardez si vous enchaînez des sélections hiérarchiques. Vous le ramenez à plat dès qu’un humain ou un merge doit lire les clés comme des champs.

  15. 15Comment lisez-vous un CSV trop gros pour la mémoire ?Seniorpandas

    On ne charge pas le fichier entier : usecols, dtype explicites, puis chunks. Le schéma se décide avant, pas par inférence sur un échantillon.

    Lire la réponse détaillée
  16. 16Que change Parquet par rapport à un CSV dans Pandas ?Intermédiairepandas

    Parquet est colonnaire, typé et compressé. Un CSV est une suite de lignes de texte sans schéma. Ce n’est pas un détail de format : c’est ce que vous relisez, ce que vous payez en CPU, et ce que vous croyez comme types le lendemain.

    À l’écriture, Pandas (via pyarrow) pose le dtype dans le pied de fichier. À la lecture, Int64 reste Int64, datetime64 reste une date, category peut se reconstruire. Avec un CSV, vous rejouez l’inférence — et un "N/A" apparu mardi transforme une colonne d’entiers en object.

    python
    df.to_parquet("ventes.parquet", index=False)
    df = pd.read_parquet(
        "ventes.parquet",
        columns=["date", "pays", "montant"],
        filters=[("pays", "=", "CA")],
    )

    columns ne lit que ces colonnes sur disque — le pendant sérieux de usecols, sans parser quarante champs texte. filters (pyarrow) pousse le prédicat vers les row groups : vous ne désérialisez pas le reste.

    La compression (snappy, zstd) réduit le volume d’un facteur souvent supérieur à cinq sur des colonnes répétitives. Le CPU de décompression est largement gagné contre le parse de CSV.

    Limites à énoncer : un Parquet n’est pas un fichier que le métier ouvre dans Excel ; les listes et structs imbriqués demandent pyarrow, pas une pensée « table plate » ; et un mauvais schéma figé se reproduit aussi fidèlement qu’un bon. Parquet préserve, il ne corrige pas.

    La phrase d’entretien : CSV pour l’échange avec un humain ou un système ancien ; Parquet dès que le fichier est un contrat entre machines.

  17. 17À quoi sert explode sur une colonne qui contient des listes ?Intermédiairepandas

    explode dénormalise : une cellule qui contient une liste (ou un tableau) devient une ligne par élément. Les autres colonnes sont recopiées. C’est le geste juste après un JSON imbriqué, un champ « tags », une cellule Excel « a; b; c » déjà découpée.

    python
    df = pd.DataFrame(
        {"commande_id": [1, 2], "skus": [["A", "B"], ["A"]]}
    )
    lignes = df.explode("skus", ignore_index=True)

    Deux commandes, trois lignes. commande_id 1 apparaît deux fois. C’est voulu — et c’est le piège : tout agrégat ultérieur doit redéfinir le grain. Un nunique de commandes reste correct ; un len(df) plus tard compte des lignes SKU, plus des commandes.

    ignore_index=True évite un index répété illisible. Les NA et listes vides : une liste vide produit une ligne avec NA dans la colonne explosée (selon la version, vérifiez sur un cas vide). Vous décidez si cette ligne survit.

    Souvent en amont :

    python
    df["skus"] = df["skus_bruts"].str.split(";")
    df = df.explode("skus")
    df["skus"] = df["skus"].str.strip()

    json_normalize + explode est le duo classique d’un export d’API. Vous n’écrivez pas de double boucle Python pour ça.

    Ce que vous dites en entretien : explode est une jointure contre les éléments d’une liste. Comme toute jointure one-to-many, elle multiplie les lignes. Vous comptez avant et après.

  18. 18Que permettent les méthodes string de Pandas sur une colonne texte ?Juniorpandas

    L’accesseur .str vectorise les opérations de chaînes en respectant les NA. lower, strip, contains, replace, slice, split, extract : vous restez dans Pandas au lieu d’un apply de méthodes Python qui explosent sur un NA.

    python
    df["email"] = df["email"].str.strip().str.lower()
    df["domaine"] = df["email"].str.split("@").str[-1]
    masque = df["email"].str.contains(r"@", na=False)
    extrait = df["ref"].str.extract(r"^([A-Z]{2})-(\d+)$", expand=True)

    Trois détails que l’intervieweur aime :

    • na=False dans contains. Sinon le masque est un booléen nullable et df[masque] se comporte mal.
    • Le dtype. .str attend du texte (object de chaînes ou string). Sur un float mal inféré (codes postaux), ça lève ou produit des NA. D’où le typage avant le nettoyage.
    • extract avec un groupe. C’est souvent plus sûr qu’un split fragile. Deux groupes → deux colonnes.

    .str.split(..., expand=True) écarte les morceaux en colonnes ; sans expand, vous obtenez une colonne de listes, éventuellement à explode.

    Vous ne faites pas for x in df["email"]. Vous enchaînez .str, vous comptez les longueurs aberrantes (df["code"].str.len().value_counts()), et vous traitez le reste comme un problème de source, pas de syntaxe.

  19. 19Comment utilisez-vous une fenêtre glissante (rolling) ?Intermédiairepandas

    rolling calcule une statistique sur les k dernières observations (ou une durée), à chaque ligne. Moyenne mobile, somme sur 7 jours, max des 24 dernières heures : la fenêtre avance, le grain des lignes ne change pas.

    Deux préconditions, sinon le chiffre est un artefact :

    1. L’ordre. Vous triez par le temps (ou par la clé + le temps). Un rolling(7) sur un fichier non trié moyenne sept lignes voisines dans le fichier, pas sept jours.
    2. Le sens de la fenêtre. Entier 7 = sept lignes. Chaîne '7D' = sept jours calendaires, et l’index doit être datetime.
    python
    s = (
        df.sort_values(["client_id", "date"])
          .set_index("date")
          .groupby("client_id")["montant"]
          .rolling("7D", min_periods=1)
          .sum()
          .reset_index()
    )

    min_periods=1 évite des NA au début de série quand vous acceptez une fenêtre partielle. Le défaut, souvent égal à la taille, masque les premiers points — parfois voulu (il faut une semaine complète), parfois un trou silencieux.

    center=True centre la fenêtre sur la ligne courante : utile en exploration, dangereux si vous expliquez un indicateur « connu à date t » — vous avez regardé le futur. En entretien data, vous le dites.

    rolling n’est pas resample : resample réindexe sur une grille (une ligne par semaine). expanding part du début et ne jette rien. ewm pondère l’historique. Vous choisissez l’objet selon la question métier, pas selon le nom qui sonne « série temporelle ».

  20. 20Qu’est-ce que le broadcasting en NumPy ?Intermédiairepandas

    Le broadcasting est la règle qui permet d’opérer deux tableaux de formes différentes sans les répliquer en mémoire, en alignant les dimensions depuis la droite.

    Deux dimensions sont compatibles si elles sont égales, ou si l’une vaut 1. NumPy « étire » conceptuellement celle qui vaut 1. (n, 1) + (1, m) donne (n, m) : une colonne plus une ligne, une addition par paire, zéro copie des opérandes.

    python
    col = np.arange(3).reshape(3, 1)   # (3, 1)
    lig = np.arange(4).reshape(1, 4)   # (1, 4)
    col + lig                          # (3, 4)

    Si une paire de dimensions n’est ni égale ni 1, c’est ValueError: operands could not be broadcast. L’erreur est un cadeau : mieux vaut ça qu’un alignement silencieux.

    La phrase qui distingue un profil data : Pandas aligne sur les étiquettes ; NumPy aligne sur la position et le broadcasting. series_a + series_b joint les index. series_a.to_numpy() + series_b.to_numpy() additionne la i-ème case, même si les index n’ont rien à voir. Sortir en NumPy « pour la vitesse » sans vérifier l’ordre des lignes est une erreur de senior, pas un détail.

    Vous utilisez le broadcasting pour une normalisation par colonne (X - X.mean(axis=0)), un masque, une grille. Vous ne tile pas à la main un tableau pour le ramener à la même forme : si le broadcast suffit, le tile est du gaspillage mémoire.

  21. 21Pourquoi préférez-vous un tableau NumPy à une liste Python pour le calcul ?Juniorpandas

    Parce qu’un tableau NumPy est homogène, contigu, et opéré hors de l’interpréteur. Une liste Python est un tableau de pointeurs vers des objets quelconques. Additionner deux listes de flottants, c’est une boucle Python, un million de boîtes float, un million de allocations. Additionner deux ndarray float64, c’est une passe C (ou SIMD) sur des octets alignés.

    python
    xs = list(range(1_000_000))
    # [x * 2 for x in xs]          # Python, objet par objet
    np.arange(1_000_000) * 2       # une instruction vectorisée

    Sur un calcul numérique typique, l’écart se compte en facteurs, pas en pourcents. C’est pour ça que Pandas s’appuie sur NumPy (ou Arrow) en dessous, et que apply retombe dans le monde des listes.

    Contreparties à énoncer, pour ne pas sonner naïf :

    • Un seul dtype. Mélanger un entier, une chaîne et un None ramène à object : vous avez perdu l’avantage, il reste l’API.
    • Taille fixe. Agrandir un tableau recopie. Une liste append est faite pour ça. Vous préallouez, ou vous collectez en liste puis np.array.
    • Petit n. En dessous de quelques milliers d’éléments, le coût d’une boucle est souvent invisible. La lisibilité gagne.

    En entretien : NumPy (et Pandas) dès que vous calculez sur un vecteur. Liste Python pour collecter, pour de l’hétérogène vrai, ou pour un graphe d’objets. Pas de for sur df["montant"].

  22. 22Quand décidez-vous que Pandas ne suffit plus ?Seniorpandas

    Quand les données ne tiennent plus en mémoire sur une machine, ou que le traitement doit être distribué : Polars sur une machine, Spark au-delà.

    Lire la réponse détaillée
  23. 23Quand query ou eval sont-ils utiles, et quels sont leurs pièges ?Intermédiairepandas

    query filtre avec une expression en chaîne. eval calcule une expression (souvent arithmétique) sans enchaîner des objets intermédiaires. Les deux existent pour la lisibilité, et parfois pour laisser numexpr vectoriser une formule.

    python
    out = df.query("montant > 0 and pays == 'CA'")
    df["ttc"] = df.eval("ht * (1 + tva)")

    Utile quand le filtre a plusieurs clauses et que df.loc[(df.a > 0) & (df.b == "CA")] devient du bruit de parenthèses. Les colonnes s’appellent par leur nom, & / | deviennent and / or dans query.

    Pièges, à citer dans cet ordre :

    • Noms de colonnes. Un espace, un tiret, un mot réservé : il faut des backticks dans l’expression (query("code postal == 'H2X'")). Si les noms sont sales, query se lit plus mal que loc.
    • Injection. L’expression est du texte. Une valeur qui vient de l’utilisateur ne se concatène pas. Vous passez des variables avec @ : df.query("montant > @seuil").
    • Ce n’est pas plus clair pour tout le monde. Une équipe qui ne connaît que loc lira loc. La performance n’est un argument que sur de très grandes frames et des formules numériques — ce n’est pas une raison de tout réécrire.
    • eval n’exécute pas de Python arbitraire. C’est un plus. N’y cherchez pas un apply déguisé.

    En entretien : vous les utilisez pour un filtre ou une formule stable, écrite par vous, pas comme un mini-SQL universel. Le jour où l’expression est générée, vous revenez à loc.

  24. 24Pourquoi enchaîner assign et pipe plutôt que modifier le DataFrame sur place ?Intermédiairepandas

    Parce qu’un enchaînement nomme une transformation et laisse l’objet précédent intact. assign ajoute ou remplace des colonnes en renvoyant un nouveau DataFrame. pipe y insère votre fonction, pour que le notebook se lise comme une recette plutôt que comme une suite de mutations.

    python
    def hors_taxes(d):
        return d.loc[d["montant"] > 0]
    
    df_propre = (
        df.rename(columns={"amt": "montant"})
          .assign(
              pays=lambda d: d["pays"].str.upper(),
              annee=lambda d: d["date"].dt.year,
          )
          .pipe(hors_taxes)
    )

    Les lambda d voient le DataFrame courant de la chaîne, pas le df d’origine — indispensable dès qu’une colonne vient d’être créée.

    Ce que ça évite :

    • les df2, df3, df_final_v2 dont plus personne ne sait lequel est vivant ;
    • un inplace au milieu qui casse la chaîne et renvoie None ;
    • de muter un DataFrame encore utilisé plus haut (vue, cache, test).

    Ce n’est pas un dogme fonctionnel. Un script de quinze lignes peut réassigner df = df[...]. Dès que le nettoyage a des étapes nommées, pipe est le lieu où vous mettez une assertion, un log de shape, une quarantaine.

    En entretien : enchaîner, ce n’est pas du style, c’est un contrat — chaque étape est une fonction de DataFrame vers DataFrame, donc testable. Muter sur place est plus court à taper et plus long à relire le jour de l’incident.

  25. 25Pourquoi évitez-vous inplace=True ?Juniorpandas

    inplace ne garantit pas d’économiser la mémoire, casse l’enchaînement et renvoie None. On réassigne le résultat d’une méthode qui renvoie un nouvel objet.

    Lire la réponse détaillée
  26. 26Comment testez-vous qu’un nettoyage n’a pas cassé les données ?Seniorpandas

    Un nettoyage sans test est une histoire que vous vous racontez. Vous figez des invariants avant de transformer, puis vous les assertiez après.

    Trois familles, suffisantes en entretien.

    La forme. Nombre de lignes dans une bande attendue, colonnes présentes, dtypes convenus. Un merge qui double les lignes, un explode oublié, un dropna trop large : shape le dit.

    Les comptes métier. Somme des montants (quand le nettoyage ne doit pas en inventer), nombre de clés distinctes, taux de NA maximal sur une colonne obligatoire, unicité d’une clé.

    Un jeu figé. Un petit CSV ou un DataFrame dans le test, avec le cas sale connu (doublon partiel, date inversée, code postal flottant). Vous n’attendez pas le prochain export de prod pour découvrir la régression.

    python
    avant = df["montant"].sum(min_count=1)
    # ... transformations qui ne doivent pas créer de montant ...
    assert df.shape[0] <= n0
    assert df["email"].is_unique
    assert df["email"].isna().mean() < 0.01
    assert df["inscrit_le"].notna().all()
    assert np.isclose(df["montant"].sum(), avant)
    assert df["pays"].dtype == "category"

    assert dans le notebook, comme dans nettoyer des données sales, vaut mieux qu’un « ça a l’air propre ». En pipeline, ces asserts deviennent un test pytest sur une fixture.

    Ce que vous ne faites pas : comparer uniquement head(5) avant / après. Cinq lignes ne voient pas la jointure qui explose à la 200 000ᵉ. Et vous n’assertiez pas une somme si vous avez volontairement filtré : l’invariant doit matcher l’intention, sinon le test ment dans l’autre sens.

  27. 27Comment définissez-vous un doublon avant de le supprimer ?Juniorpandas

    Un doublon n’est pas « deux lignes qui se ressemblent ». C’est deux lignes qui représentent la même entité selon une clé que vous avez écrite. Sans cette clé, drop_duplicates() est une perte au hasard.

    Trois définitions courantes :

    • Exactes — toutes les colonnes identiques. Rare en vrai, utile pour un export collé deux fois.
    • Même clé métier — même email, même commande_id. Le reste peut diverger (adresse, statut, horodatage).
    • Même clé, contradiction — même email, deux pays. Ce n’est plus un doublon à écraser : c’est une quarantaine.
    python
    dupes = df[df.duplicated(subset=["email"], keep=False)].sort_values("email")

    keep=False montre toutes les copies, pas seulement les suivantes. Vous lisez dupes avant de jeter.

    Puis une règle explicite, pas keep="first" sur l’ordre du fichier :

    python
    df = (
        df.sort_values("mis_a_jour_le")
          .drop_duplicates(subset=["email"], keep="last")
    )

    C’est le tri qui décide qui survit, pas Pandas. Le tutoriel nettoyer des données sales le dit ainsi, et c’est la phrase à reproduire en entretien.

    df["email"].is_unique après coup vérifie le contrat. Si la clé est (email, source), l’unicité n’est pas celle que vous croyez : vous le nommez dans subset.

  28. 28Pourquoi une jointure peut-elle multiplier le nombre de lignes ?Seniorpandas

    Un many-to-many multiplie les lignes : chaque paire de clés qui matchent produit une ligne. On valide le type de relation et on compare les effectifs avant et après.

    Lire la réponse détaillée
  29. 29Dans quel ordre prenez-vous les décisions pour nettoyer des données sales ?Seniorpandas

    On décide d’abord le sens métier des manquants, des doublons et des types, puis on transforme dans un ordre qui ne détruit pas l’information.

    Lire la réponse détaillée
  30. 30Que regardez-vous sur un DataFrame avant d’écrire la première ligne de code ?Intermédiairepandas

    Dtypes, taux de NA et cardinalité, avant la première transformation. Sinon on code à l’aveugle sur un tableau mal typé.

    Lire la réponse détaillée