Données manquantes — trouver les valeurs manquantes, puis décider de ce qu'elles signifient
Trouver chaque valeur manquante dans un tableau, y compris celles masquées sous forme de texte comme - ou ?, comprendre comment NaN modifie les sommes, les moyennes et les types, et choisir délibérément — colonne par colonne — de supprimer, remplir ou conserver chaque manque.
Comment voulez-vous apprendre ?
Le problème que nous résolvons
Les commandes de la boutique pour la première semaine de mars ont été saisies à la main dans trois succursales et exportées sous le nom orders_raw.csv. Le propriétaire souhaite obtenir deux chiffres d'ici ce soir : combien d'unités ont été vendues et combien d'argent est entré.
Vous lisez le fichier et, par habitude prise dans les chapitres précédents, vous faites d'abord la somme de la colonne des prix, simplement pour vous faire une idée de l'échelle :
import pandas as pd
orders = pd.read_csv("orders_raw.csv")
print(orders["price"].sum())15.060.0850.08.0-15.0120.0Ce n'est pas un nombre. Ce sont tous les prix collés les uns aux autres sous forme de texte — et pandas n'a levé aucune erreur pour vous en avertir. Voyons maintenant la colonne des quantités :
print(orders["quantity"].sum())
print(orders["quantity"].mean())76.0
10.857142857142858Celui-ci semble correct, et c'est encore pire. Il y a huit commandes dans le fichier, or 76 / 8 donne 9.5, et non 10.86. Une quantité est vide, et pandas l'a silencieusement écartée du total et de la moyenne. Personne ne vous l'a dit non plus.
Le fichier comporte donc des manques, et ces manques se présentent sous deux formes. Certains sont visibles : une cellule vide, que pandas transforme en un marqueur spécial de « valeur manquante » puis ignore en silence lors des calculs arithmétiques. D'autres sont masqués : quelqu'un a saisi - au lieu de laisser la cellule vide, pandas l'a pris au pied de la lettre, et toute la colonne des prix est devenue du texte.
Aucun des deux types ne lève d'erreur. Les deux modifient le résultat. Ce chapitre a pour objet de repérer chaque manque, de comprendre comment pandas le traite, puis de prendre une décision délibérée — le supprimer, le remplir ou le conserver — au lieu de laisser le comportement par défaut décider à votre place.
À la fin de ce chapitre, vous saurez
- Expliquer ce qu'est
NaN, pourquoiNaN == NaNvautFalse, et pourquoi une colonne de nombres entiers devient décimale lorsqu'une cellule est vide - Compter les valeurs manquantes par colonne avec
isna().sum()et extraire les lignes incomplètes - Transformer des marqueurs masqués comme
-ou?en véritables valeurs manquantes, avecna_values=oupd.to_numeric(errors="coerce") - Prédire comment
sum,mean,countet l'arithmétique de colonnes traitent les valeurs manquantes - Supprimer les valeurs manquantes avec
dropna(), en utilisantsubset=ethow=pour ne retirer que ce que vous souhaitez - Remplacer les valeurs manquantes avec
fillna()— une valeur, une valeur différente par colonne ou une statistique - Décider, colonne par colonne, s'il faut supprimer, remplir ou conserver — et justifier votre choix
- Éviter le piège de
inplace=Truequi ne fait rien sous pandas 3
Prérequis : Filtrer des lignes.
Créer le fichier d'abord
Dans votre dossier de projet, créez orders_raw.csv avec exactement ces lignes. Les manques sont délibérés — regardez attentivement les troisième, cinquième et septième lignes :
order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,,bottle,accessories,7,N/A
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,-
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0Il s'agit des huit mêmes commandes que dans orders.csv des chapitres précédents, avec quatre anomalies : la commande 1002 n'a pas de quantité, la commande 1004 n'a pas de succursale et son prix indique N/A, et la commande 1006 a - comme prix.
Avant d'écrire le code
Les données manquantes sont le seul sujet où commencer par écrire du code est directement dangereux : dropna() et fillna() s'exécutent tous deux sans broncher et modifient tous deux votre résultat. Le travail commence donc par l'observation et la prise de décision.
1. Formulez la question en une phrase. « Quelle est la quantité totale vendue, et quel est le chiffre d'affaires total (quantité × prix), pour la semaine — et combien de commandes n'ont pas pu être comptabilisées ? » La dernière partie a toute son importance. Un total qui écarte silencieusement deux commandes n'est pas la même réponse qu'un total précisant « 6 commandes sur 8 comptabilisées ».
2. Observez ce qui est arrivé, avant toute autre chose. shape et info() vous renseignent ensemble sur chaque colonne d'un seul coup :
print(orders.shape)
orders.info()(8, 7)
<class 'pandas.DataFrame'>
RangeIndex: 8 entries, 0 to 7
Data columns (total 7 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 order_id 8 non-null int64
1 date 8 non-null str
2 branch 7 non-null str
3 product 8 non-null str
4 category 8 non-null str
5 quantity 7 non-null float64
6 price 7 non-null str
dtypes: float64(1), int64(1), str(5)
memory usage: 580.0 bytesLisez cela en le comparant à ce que vous attendiez, colonne par colonne :
branch— 8 valeurs non nulles de typestrattendues, 7 non nulles obtenues. Une succursale est manquante.quantity— 8 valeurs non nulles de typeint64attendues, 7 non nulles de typefloat64obtenues. Une quantité est manquante, et c'est la raison pour laquelle elle est devenuefloat64(expliqué plus bas).price— 8 valeurs non nulles de typefloat64attendues, 7 non nulles de typestrobtenues. Un prix est manquant et du texte s'est glissé dans la colonne.
Deux éléments de cette liste sont des indices plutôt que des faits bruts. Une colonne de nombres entiers affichant float64 indique presque toujours un manque. Une colonne de nombres affichant str signifie toujours que du texte y est mélangé — et du texte dans une colonne de nombres est généralement un marqueur de « valeur manquante » masqué.
3. Esquissez la sortie souhaitée. Quelque chose comme :
Missing values per column: branch 1, quantity 1, price 2
Orders counted for revenue: 6 of 8
Total quantity (known): 76
Total revenue (counted): ...Remarquez qu'elle signale les manques tout autant que les totaux. Quiconque lit le rapport doit savoir que le chiffre d'affaires constitue une borne inférieure.
4. Choisissez l'outil pour chaque manque — avant de l'écrire. Pour chaque colonne, demandez-vous : que signifie une cellule vide ici, et peut-on encore répondre à la question sans elle ?
branch— un manque signifie que la commande a eu lieu mais que la succursale n'a pas été enregistrée. Décision : remplir avec"Unknown", car la commande est bien réelle et son montant compte toujours.quantity— un manque signifie que nous ignorons combien d'unités ont été vendues. Décision : conserver la valeur manquante et exclure la ligne du chiffre d'affaires, car inventer un nombre (0 ? la moyenne ?) reviendrait à inventer des ventes.price— un manque signifie que nous ignorons à quel prix l'article s'est vendu. Décision : conserver la valeur manquante et exclure la ligne du chiffre d'affaires, pour la même raison — et indiquer combien de lignes ont été exclues.
5. Décidez de ce que vous vérifierez ensuite. Après chaque étape qui supprime ou remplace des valeurs, affichez de nouveau le décompte des manques et la forme (shape). Si un appel à dropna() vous fait passer de 8 lignes à 2, vous devez le voir immédiatement, pas une fois le rapport diffusé.
Le reste du chapitre vous présente chaque outil tour à tour, et nous réunirons ce plan en un seul programme à la fin.
À quoi ressemble une valeur manquante : NaN
Lorsque pandas rencontre une cellule vide, il ne stocke ni une chaîne vide ni zéro. Il stocke une valeur spéciale à virgule flottante appelée NaN — « not a number » (pas un nombre). Vous pouvez en créer une vous-même en insérant None dans une Series :
import pandas as pd
quantity = pd.Series([12, None, 2])
print(quantity)0 12.0
1 NaN
2 2.0
dtype: float64Regardez le dtype. Vous lui avez fourni deux nombres entiers et un manque, et vous avez obtenu en retour du float64, avec 12.0 et 2.0. C'est la raison qui explique l'indice dans info() : NaN est une valeur float, et une colonne d'entiers classique n'a aucun moyen de la contenir. Dès qu'une cellule est manquante, pandas promeut donc la colonne entière en float64 pour faire de la place à NaN. C'est pour cette raison que quantity dans notre fichier s'est affiché avec 12.0, 2.0, et ainsi de suite.
Les colonnes de texte se comportent différemment. Une colonne str peut contenir une valeur manquante sans changer de type :
branch = pd.Series(["north", None, "east"])
print(branch)0 north
1 NaN
2 east
dtype: strElle s'affiche toujours comme NaN, et le dtype reste str. Ainsi, dans pandas 3, vous rencontrerez NaN aussi bien dans les colonnes numériques que dans les colonnes de texte, et les mêmes outils le repèrent dans les deux.
NaN n'est égal à rien — pas même à lui-même
Il s'agit du fait le plus surprenant concernant les valeurs manquantes, et il met en échec la façon la plus naturelle de les chercher :
import numpy as np
import pandas as pd
orders = pd.read_csv("orders_raw.csv")
print(np.nan == np.nan)
print((orders["branch"] == np.nan).sum())False
0NaN signifie « une valeur que nous ne connaissons pas ». Deux valeurs inconnues ne sont pas réputées égales, la comparaison indique donc False — à chaque fois. Cela fait de == np.nan un test qui ne correspond à rien, jamais : la colonne des succursales comporte un manque, et le décompte affiche 0. Interrogez plutôt pandas directement.
Ne cherchez jamais les valeurs manquantes avec==.== np.nans'exécute sans erreur et ne trouve jamais rien, de sorte qu'une colonne criblée de manques semble complète. Utilisezisna().
Trouver les valeurs manquantes : isna()
isna() pose la question que == ne peut pas poser : « cette cellule est-elle manquante ? » Sur une colonne, elle renvoie une colonne de True/False — un masque, exactement semblable à ceux du chapitre sur le filtrage :
print(orders["branch"].isna())0 False
1 False
2 False
3 True
4 False
5 False
6 False
7 False
Name: branch, dtype: boolSur le DataFrame entier, elle fait de même pour chaque cellule. Cependant, vous avez rarement envie de lire huit lignes de True/False. Vous voulez un décompte, et comme True compte pour 1, .sum() vous le fournit :
print(orders.isna().sum())order_id 0
date 0
branch 1
product 0
category 0
quantity 1
price 1
dtype: int64C'est la ligne à exécuter après info() sur n'importe quel nouveau fichier. Il s'agit de la même information que le Non-Null Count, inversée pour compter directement les manques.
Mais observez price : elle indique 1. Nous savons pourtant que deux prix posent problème — N/A et -. Gardez cela en tête ; nous y reviendrons dans la section suivante.
Quelles lignes sont incomplètes ?
Un décompte indique combien il y en a ; pour voir lesquelles, utilisez le masque comme filtre. isna().any(axis=1) demande, pour chaque ligne, « y a-t-il quelque chose de manquant dans cette ligne ? » :
incomplete = orders[orders.isna().any(axis=1)]
print(incomplete)order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery NaN 60.0
3 1004 2024-03-02 NaN bottle accessories 7.0 NaNaxis=1 signifie « regarder le long de chaque ligne » plutôt que de haut en bas le long de chaque colonne. Et remarquez qui n'est pas dans la liste : la commande 1006, dont le prix est -. Pour autant que pandas le sache, cette ligne est complète. Gardez cette idée sous le coude. Examiner les lignes réelles vaut bien une ligne de code supplémentaire : c'est ainsi que vous remarquez, par exemple, que chaque quantité manquante provient de la même succursale, ce qui constitue une question pour les personnes de cette succursale plutôt que pour pandas.
En proportion, pas en décompte
Avec huit lignes, les décomptes conviennent très bien. Avec quatre-vingt mille lignes, « 312 manquantes » ne veut pas dire grand-chose tant que vous ne savez pas que cela représente 0.4 %. .mean() sur une colonne de True/False donne la fraction de True :
print(orders.isna().mean())order_id 0.000
date 0.000
branch 0.125
product 0.000
category 0.000
quantity 0.125
price 0.125
dtype: float64Une cellule sur huit dans branch, quantity et price est manquante — 12.5 % chacune. Une colonne vide à 90 % et une colonne vide à 0.1 % exigent des décisions complètement différentes, et c'est ainsi que vous les distinguez.
Valeurs manquantes masquées : quand le manque est écrit sous forme de texte
isna() a compté un prix manquant, mais deux prix sont inutilisables. Le second est le - dans la commande 1006. Pour pandas, - n'est pas manquant — c'est un morceau de texte tout à fait valable. Et un seul morceau de texte dans une colonne de nombres suffit à transformer la colonne entière en str, ce que info() a exactement montré.
Voici comment découvrir ce qui se trouve réellement dans une colonne :
print(orders["price"].unique())<StringArray>
['15.0', '60.0', '850.0', nan, '8.0', '-', '120.0']
Length: 7, dtype: strChaque valeur est entre guillemets — ce sont toutes du texte, '15.0' y compris. Et deux sortes de manques se côtoient : nan, que pandas a reconnu, et '-', qu'il n'a pas reconnu.
Pourquoi N/A est-il devenu nan mais pas - ? Parce que read_csv est fourni avec une liste intégrée de chaînes qu'il traite comme manquantes : une cellule vide, NA, N/A, n/a, NaN, null, None et quelques autres. -, ?, missing, 0 et unknown ne figurent pas sur cette liste. Ce que la personne saisissant les données a utilisé pour signifier « ne sait pas » détermine le type de manque que vous obtenez.
Il existe deux manières de transformer les valeurs masquées en véritables NaN.
Solution 1 : signaler le marqueur à read_csv — na_values=
Si vous connaissez le marqueur, la solution la plus propre s'applique au moment de la lecture. na_values= ajoute vos chaînes à la liste intégrée :
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
print(orders["price"].dtype)
print(orders.isna().sum())float64
order_id 0
date 0
branch 1
product 0
category 0
quantity 1
price 2
dtype: int64Comparez ce résultat avec le décompte précédent. branch et quantity sont inchangées — na_values= s'ajoute à la liste, sans la remplacer, de sorte que les cellules vides et N/A sont toujours reconnues. Ce qui a changé, c'est price : elle est désormais de type float64, et elle signale 2 valeurs manquantes — N/A et - sont désormais toutes deux de vrais NaN. La colonne est redevenue numérique, ce qui signifie que les calculs arithmétiques effectués sur elle sont de véritables calculs.
Notez que na_values=["-"] s'applique à chaque colonne. Ici, c'est ce que nous voulons. Si un - pouvait constituer une valeur réelle ailleurs — un code produit, par exemple —, passez plutôt un dictionnaire, na_values={"price": ["-"]}, et seule la colonne price sera concernée.
Solution 2 : convertir après coup — pd.to_numeric(errors="coerce")
Parfois, vous ne savez pas à l'avance quelles chaînes bizarres se trouvent dans une colonne, ou le fichier a été lu par le code de quelqu'un d'autre. Dans ce cas, vous convertissez la colonne après la lecture. Observez d'abord ce que fait une conversion ordinaire :
raw = pd.read_csv("orders_raw.csv")
price = pd.to_numeric(raw["price"])ValueError: Unable to parse string "-" at position 5Cette erreur est utile : elle nomme la valeur fautive et sa position. Ajouter errors="coerce" indique « transformez en NaN tout ce que vous ne parvenez pas à convertir en nombre » :
price = pd.to_numeric(raw["price"], errors="coerce")
print(price)0 15.0
1 60.0
2 850.0
3 NaN
4 8.0
5 NaN
6 15.0
7 120.0
Name: price, dtype: float64Pour modifier effectivement le tableau, réassignez le résultat à la colonne — to_numeric renvoie une nouvelle Series et laisse raw intact :
raw["price"] = pd.to_numeric(raw["price"], errors="coerce")
print(raw["price"].dtype, raw["price"].isna().sum())float64 2Quelle solution choisir
errors="coerce" est puissant et radical. Il transforme tout élément non numérique en NaN — y compris un prix réel mal saisi comme 1O0 (avec la lettre O) ou 1,200 avec une virgule de séparation des milliers. Ce ne sont pas des valeurs manquantes ; ce sont des valeurs contenant une faute de frappe, et les forcer ainsi élimine silencieusement de l'information.
Prenez donc cette habitude : comptez avant, comptez après, et expliquez la différence.
- Vous connaissez le marqueur (
-,?,missing) : utilisezna_values=dansread_csv. C'est précis — seule cette chaîne devient une valeur manquante. - Résidus inconnus, et la colonne doit être numérique : utilisez
pd.to_numeric(errors="coerce"). Cela capture tout, vérifiez donc ce qui a été capturé. - Certaines valeurs sont des fautes de frappe que vous pouvez corriger : examinez d'abord
unique(), corrigez-les, puis convertissez. Forcer la conversion détruirait de véritables données.
Avant le forçage, raw["price"].unique() montrait exactement une chaîne non numérique ('-') plus un nan. Après le forçage, il y a exactement 2 NaN. Les nombres concordent, rien d'autre n'a donc été perdu. Cette vérification fait toute la différence entre nettoyer des données et les détériorer.
À partir d'ici, nous utilisons la version lue avec na_values=["-"].
Comment les calculs traitent NaN
Maintenant que les manques sont de véritables NaN, vous devez savoir ce que pandas en fait — car il ne s'arrêtera pas pour vous demander votre avis.
Les résumés de colonnes ignorent NaN
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
q = orders["quantity"]
print("len :", len(q))
print("count:", q.count())
print("sum :", q.sum())
print("mean :", q.mean())
print("check:", q.sum() / q.count())len : 8
count: 7
sum : 76.0
mean : 10.857142857142858
check: 10.857142857142858Trois comportements à retenir :
lencompte les lignes ;count()compte les valeurs. La différence entre les deux représente le nombre de manques. C'est le même principe que leNon-Null Countdansinfo().sum()ignoreNaN. Elle additionne les sept quantités connues. C'est raisonnable — mais cela signifie que « 76 » est le total des quantités connues, et non de toutes les commandes.mean()divise parcount(), et non parlen. La moyenne est calculée sur les sept valeurs connues. C'est généralement ce que l'on souhaite ; traiter le manque comme un 0 tirerait la moyenne vers le bas sans aucune raison.
Si vous préférez être averti qu'un élément est manquant, skipna=False renvoie NaN dès qu'une valeur l'est :
print(q.sum(skipna=False))nanL'arithmétique entre colonnes propage NaN
Les résumés ignorent les manques ; l'arithmétique les propage. Toute combinaison avec NaN produit NaN, car « 7 fois un prix inconnu » est lui-même inconnu :
revenue = orders["quantity"] * orders["price"]
print(revenue)
print("total:", revenue.sum())
print("rows counted:", revenue.count(), "of", len(revenue))0 180.0
1 NaN
2 1700.0
3 NaN
4 240.0
5 NaN
6 300.0
7 480.0
dtype: float64
total: 2900.0
rows counted: 5 of 8Rapprochez ces deux comportements et vous constaterez le piège évoqué au début du chapitre. revenue.sum() a affiché 2900.0 sans aucun avertissement — alors que trois des huit commandes n'ont apporté aucune contribution. La commande 1002 manque d'une quantité, les commandes 1004 et 1006 manquent d'un prix. Le total n'est honnête que si vous précisez « 5 commandes sur 8 ». C'est pourquoi le plan demandait d'indiquer un décompte à côté de chaque total.
Supprimer les valeurs manquantes : dropna()
dropna() supprime les lignes qui contiennent NaN. Sans argument, elle supprime chaque ligne comportant le moindre manque dans n'importe quelle colonne :
print(orders.shape)
print(orders.dropna().shape)(8, 7)
(5, 7)Trois lignes sur huit ont disparu — et la commande 1004 a été éliminée parce que sa succursale était manquante, alors même que pour une question portant sur la quantité, il s'agissait d'une ligne tout à fait exploitable. C'est là tout le problème d'un simple dropna() : il répond à la consigne « supprimer tout ce qui est imparfait », ce qui correspond rarement à la question posée.
subset= — uniquement les colonnes nécessaires à la question
Précisez quelles colonnes doivent obligatoirement être présentes :
known_qty = orders.dropna(subset=["quantity"])
print(known_qty.shape)
print(known_qty["quantity"].sum())(7, 7)
76.0Seule la commande 1002 est supprimée — la seule ligne dont la quantité est véritablement inconnue. Pour le chiffre d'affaires, les deux colonnes sont nécessaires :
countable = orders.dropna(subset=["quantity", "price"])
print(countable[["order_id", "quantity", "price"]])order_id quantity price
0 1001 12.0 15.0
2 1003 2.0 850.0
4 1005 30.0 8.0
6 1007 20.0 15.0
7 1008 4.0 120.0Remarquez l'index : 0, 2, 4, 6, 7. dropna() conserve les étiquettes d'origine, tout comme le fait le filtrage, ce qui vous permet de toujours rattacher une ligne au fichier source. Si vous avez de nouveau besoin de 0, 1, 2…, ajoutez .reset_index(drop=True).
how="all" — uniquement les lignes entièrement vides
Les feuilles de calcul exportées se terminent souvent par quelques lignes vides dans toutes les colonnes. how="all" supprime uniquement celles-ci et conserve toute ligne contenant ne serait-ce qu'une seule valeur :
print(orders.dropna(how="all").shape)(8, 7)Rien n'a été supprimé ici — aucune de nos lignes n'est entièrement vide —, ce qui est exactement ce que vous souhaitez vérifier.
dropna() renvoie un nouveau tableau
Comme la plupart des méthodes de pandas, dropna() ne modifie pas orders ; elle vous renvoie un nouveau DataFrame. Si vous écrivez orders.dropna() seul sur une ligne, rien n'arrive à orders. Assignez le résultat — à un nouveau nom si vous avez encore besoin du tableau complet, ce qui sera le cas dans un rapport comme le nôtre (pour indiquer combien de lignes ont été écartées).
Remplacer les valeurs manquantes : fillna()
Il arrive qu'un manque puisse être remplacé de façon judicieuse. fillna() place une valeur là où se trouvait NaN.
Une valeur pour une colonne
La succursale manquante en est l'exemple le plus évident. La commande a bien eu lieu et son montant est réel ; nous ignorons simplement quelle succursale l'a enregistrée. La remplir avec un libellé conserve la ligne et rend le manque visible dans tout décompte ultérieur par succursale :
orders["branch"] = orders["branch"].fillna("Unknown")
print(orders["branch"].tolist())['north', 'south', 'north', 'Unknown', 'north', 'south', 'east', 'north']La structure de cette ligne est primordiale : prenez la colonne, appliquez fillna, et réassignez le résultat à la même colonne. fillna renvoie une nouvelle Series ; sans le orders["branch"] = devant, rien n'est enregistré.
Des valeurs différentes pour des colonnes différentes : un dictionnaire
Passez un dictionnaire {column: value} pour remplir plusieurs colonnes à la fois, chacune avec sa propre valeur. Les colonnes non mentionnées restent intactes :
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
filled = orders.fillna({"branch": "Unknown", "quantity": 0})
print(filled.isna().sum())order_id 0
date 0
branch 0
product 0
category 0
quantity 0
price 2
dtype: int64price comporte toujours ses deux manques, car nous ne l'avons pas mentionnée. C'est tout l'avantage de la forme avec dictionnaire par rapport à orders.fillna(0), qui remplirait chaque colonne, prix compris, avec 0 — or un prix de 0 correspond à un sac offert.
Remplir modifie le résultat
Remplir la quantité avec 0 semblait anodin. Comparez les moyennes :
print("skip the gap :", orders["quantity"].mean())
print("fill with 0 :", orders["quantity"].fillna(0).mean())
print("fill with median:", orders["quantity"].fillna(orders["quantity"].median()).mean())skip the gap : 10.857142857142858
fill with 0 : 9.5
fill with median: 10.375Trois moyennes différentes pour une même colonne, et la seule différence réside dans ce que vous avez décidé qu'une quantité inconnue signifiait :
- 0 affirme « cette commande n'a rien vendu ». Pour la commande 1002, une commande de carnets qui a manifestement vendu quelque chose, c'est faux, et cela tire la moyenne vers le bas.
- La médiane (7 ici) affirme « supposons une commande typique ». Elle maintient la moyenne à peu près stable, mais le total inclut désormais 7 carnets que personne n'a comptés.
- Ignorer affirme « nous ne savons pas, laissons-la donc de côté ». La moyenne est calculée sur les sept commandes que nous connaissons.
Aucune de ces options n'est « correcte » dans l'absolu. Chacune répond à une question légèrement différente. C'est pourquoi la décision doit figurer dans votre plan, par écrit, plutôt que dans n'importe quel choix par défaut adopté à la va-vite.
Une règle empirique qui fonctionne la plupart du temps :
- Un libellé (succursale, catégorie, nom) : remplissez avec
"Unknown"ou un équivalent. La ligne est conservée, et le manque reste visible comme son propre groupe. - Un décompte où vide signifie vraiment zéro (retours, réclamations) : remplissez avec
0— mais seulement quand la source veut vraiment dire zéro par une case vide. - Une mesure ou un montant d'argent (quantité vendue, prix, note) : conservez
NaN, excluez la ligne du calcul, et indiquez combien de lignes ont été exclues. Des nombres inventés donnent des totaux inventés. - Une colonne indispensable à la question, présente dans la quasi-totalité des lignes : appliquez
dropna(subset=[...])pour cette seule question. Une perte minime, et une réponse honnête.
Retrouver des nombres entiers
Après remplissage, quantity ne comporte plus aucun manque, mais reste de type float64 — pandas ne la rétablit pas de lui-même. Une fois qu'il ne reste plus aucun NaN, astype(int) fonctionne :
filled["quantity"] = filled["quantity"].astype(int)
print(filled["quantity"].tolist())
print(filled["quantity"].dtype)[12, 0, 2, 7, 30, 1, 20, 4]
int64Essayez avant le remplissage et l'opération échoue, car aucun entier ne signifie « inconnu » — voir « Quand ça plante » plus bas.
Un exemple complet
clean_orders.py rassemble le plan décrit dans « Avant d'écrire le code » en un seul programme : lire avec le marqueur connu, signaler chaque manque, remplir la succursale, exclure les lignes qui ne peuvent pas être comptabilisées, et afficher les totaux en précisant le nombre de commandes qu'ils couvrent.
import pandas as pd
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
# 1. Look first
print("Rows, columns:", orders.shape)
missing = orders.isna().sum()
print("Missing per column:")
print(missing[missing > 0])
print()
# 2. Decide per column: branch -> label; quantity and price -> keep, exclude
orders["branch"] = orders["branch"].fillna("Unknown")
# 3. Only rows with both numbers can be counted for revenue
countable = orders.dropna(subset=["quantity", "price"])
skipped = orders[orders[["quantity", "price"]].isna().any(axis=1)]
revenue = countable["quantity"] * countable["price"]
print("Orders counted for revenue:", len(countable), "of", len(orders))
print("Total quantity (known): ", int(orders["quantity"].sum()))
print("Total revenue (counted): ", revenue.sum())
print()
print("Not counted - check these with the branch:")
print(skipped[["order_id", "branch", "quantity", "price"]])Rows, columns: (8, 7)
Missing per column:
branch 1
quantity 1
price 2
dtype: int64
Orders counted for revenue: 5 of 8
Total quantity (known): 76
Total revenue (counted): 2900.0
Not counted - check these with the branch:
order_id branch quantity price
1 1002 south NaN 60.0
3 1004 Unknown 7.0 NaN
5 1006 south 1.0 NaNPourquoi ce code est écrit ainsi :
na_values=["-"]figure dès la toute première ligne. Le marqueur a été repéré au préalable en examinantunique(). Le placer dansread_csvgarantit que la colonne est numérique dès le départ, et toute personne lisant le programme voit immédiatement quel marqueur le fichier utilise.missing[missing > 0]n'affiche que les colonnes comportant des manques. Sur un fichier de quarante colonnes, c'est ce qui fait la différence entre un rapport lisible et un mur de zéros.- La succursale est remplie, pas les nombres. C'est la liste de décisions issue du plan, traduite en code. Un lecteur peut ne pas être d'accord, mais la démarche est transparente sous ses yeux.
dropna(subset=...)nomme les deux colonnes nécessaires au chiffre d'affaires, de sorte que la commande 1004 est exclue en raison de son prix manquant, et non de sa succursale manquante.- Les lignes ignorées sont affichées, pas seulement dénombrées. « 3 commandes non comptabilisées » suscite immédiatement la question « lesquelles ? » — répondez-y dans le rapport. Ces trois commandes constituent une liste que quelqu'un peut concrètement aller vérifier et corriger.
- « Total quantity (known) » et « (counted) » précisent ce que sont ces chiffres. Tous deux constituent des bornes inférieures ; ces libellés évitent que quiconque ne les considère comme exhaustifs.
Quand ça plante
TypeError: Cannot perform reduction 'mean' with string dtype Vous avez demandé la moyenne d'une colonne que pandas a lue comme du texte — généralement parce qu'une cellule contient un marqueur tel que -, ? ou missing. Exécutez df["col"].unique() pour trouver le coupable, puis relisez le fichier avec na_values=["-"] ou convertissez avec pd.to_numeric(df["col"], errors="coerce").
raw = pd.read_csv("orders_raw.csv")
print(raw["price"].mean())TypeError: Cannot perform reduction 'mean' with string dtypesum() a renvoyé une longue chaîne de chiffres, comme 15.060.0850.0... Même cause, pire symptôme : sur une colonne de texte, sum() concatène les chaînes au lieu d'additionner des nombres, sans lever la moindre erreur. Tout total qui ressemble à cela provient d'une colonne str. Vérifiez df.dtypes avant d'additionner quoi que ce soit.
ValueError: Unable to parse string "-" at position 5 pd.to_numeric a rencontré du texte qu'il n'a pas pu convertir, et vous a indiqué exactement quoi et où. Ajoutez errors="coerce" (après avoir vérifié avec unique() que les valeurs anormales sont bien des marqueurs d'« inconnu » et non des fautes de frappe), ou corrigez d'abord les fautes de frappe.
(df["col"] == np.nan).sum() affiche 0, mais il y a des manques NaN n'est jamais égal à rien, pas même à lui-même, donc == np.nan ne correspond à aucune cellule. Utilisez df["col"].isna().
IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer Vous avez appelé astype(int) sur une colonne contenant encore NaN. Aucun entier ne signifie « inconnu ». Remplissez ou supprimez d'abord les manques, puis convertissez :
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
orders["quantity"] = orders["quantity"].astype(int)pandas.errors.IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer.Replace or remove non-finite values or cast to an integer typethat supports these values (e.g. 'Int64')(Les espaces manquants dans « integer.Replace » et « typethat » figurent dans le message propre à pandas.) Le message mentionne également 'Int64' avec un I majuscule : un type entier distinct propre à pandas qui peut contenir des valeurs manquantes. Il est utile, mais remplir ou supprimer les manques au préalable reste la solution la plus simple et celle qu'il convient d'adopter en premier.
ChainedAssignmentError — et le manque est toujours là Vous avez écrit la syntaxe avec inplace=True que l'on trouve dans de nombreux anciens tutoriels :
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
orders["quantity"].fillna(0, inplace=True)
print(orders["quantity"].isna().sum())1
ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment using an inplace method.
Such inplace method never works to update the original DataFrame or Series, because the intermediate object on which we are setting values always behaves as a copy (due to Copy-on-Write).
For example, when doing 'df[col].method(value, inplace=True)', try using 'df.method({col: value}, inplace=True)' instead, to perform the operation inplace on the original object, or try to avoid an inplace operation using 'df[col] = df[col].method(value)'.
See the documentation for a more detailed explanation: https://pandas.pydata.org/pandas-docs/stable/user_guide/copy_on_write.htmlLe décompte est toujours de 1 : rien n'a été rempli. Dans pandas 3, orders["quantity"] se comporte toujours comme sa propre copie (Copy-on-Write), de sorte que le remplir « sur place » remplit cette copie, qui est ensuite jetée. Les anciennes versions de pandas laissaient parfois fonctionner cette tournure, ce qui explique pourquoi ce motif est omniprésent en ligne. Écrivez plutôt l'assignation explicitement — cela fonctionne dans toutes les versions et montre clairement ce qui change :
orders["quantity"] = orders["quantity"].fillna(0)
print(orders["quantity"].isna().sum())0dropna() a supprimé bien plus de lignes que prévu Un simple dropna() supprime une ligne dès qu'un manque survient dans n'importe quelle colonne — y compris une colonne notes ou discount en grande partie vide que vous n'utilisiez même pas. Affichez df.isna().sum() pour repérer la colonne contenant beaucoup de manques, puis utilisez dropna(subset=[...]) avec uniquement les colonnes indispensables à votre question.
TypeError: can't multiply sequence by non-int of type 'float' Vous avez multiplié une colonne de nombres par une colonne de texte (quantity * price alors que price est de type str). La solution est identique à celle de la première erreur : rendez d'abord price numérique.
Étape 4 sur 6 — Prédire
Vérifiez votre compréhension
Une quantité est vide. Qu'est-ce qui est affiché ?
from io import StringIO
import pandas as pd
# Stands in for orders_raw.csv, so this snippet runs on its own.
RAW = """order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,,bottle,accessories,7,N/A
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,-
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0
"""
orders = pd.read_csv(StringIO(RAW))
print(orders["quantity"].sum(), orders["quantity"].count(), len(orders))- A76.0 7 8
- B76.0 8 8
- C76 8 8
- Dnan 7 8
Le but est de trouver les commandes sans prix. Deux prix sont manquants. Qu'est-ce qui est affiché ?
from io import StringIO
import pandas as pd
# Stands in for orders_raw.csv, so this snippet runs on its own.
RAW = """order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,,bottle,accessories,7,N/A
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,-
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0
"""
orders = pd.read_csv(StringIO(RAW), na_values=["-"])
missing_price = orders[orders["price"] == float("nan")]
print(len(missing_price))- A0 — `NaN` n'est égal à rien, pas même à `NaN`, donc `==` ne le trouve jamais
- B1
- CUne `TypeError` : vous ne pouvez pas comparer avec `nan`
- D2
La colonne des prix a été lue comme str car une cellule contient - et une autre N/A. Quelle ligne la convertit en nombres, avec les deux valeurs manquantes sous forme de NaN ?
- Aorders["price"] = pd.to_numeric(orders["price"], errors="coerce")
- Borders["price"] = orders["price"].fillna(0)
- Corders["price"] = orders["price"].astype(float)
- Dorders["price"] = orders["price"].dropna()
Répondre nécessite un compte
Connectez-vous pour vérifier vos réponses
Les questions sont ci-dessus, et les résoudre de tête est ce qui compte vraiment. Connectez-vous pour voir les réponses, les explications et les indices à trois niveaux.
À vous de jouer
La boutique a lancé la livraison à domicile, et le registre des livreurs pour la première semaine arrive sous le nom deliveries.csv. Créez-le exactement comme ceci :
order_id,branch,distance_km,fee,rider
2001,north,3.5,60,R1
2002,north,,60,R2
2003,south,8.0,?,R1
2004,east,12.0,120,
2005,north,2.0,40,R2
2006,south,6.5,90,?
2007,east,,110,R3
2008,north,4.0,60,R3Le propriétaire demande : combien a été collecté en frais de livraison, combien de livraisons n'ont pas de frais renseigné, et quel est le tarif moyen par kilomètre ?
Avant d'écrire la moindre ligne de code, observez le fichier avec unique() et déterminez quel est le marqueur masqué. Écrivez ensuite deliveries_report.py qui :
- Lit le fichier de manière à ce que le marqueur masqué devienne une véritable valeur manquante dans chaque colonne où il apparaît
- Affiche le nombre de valeurs manquantes par colonne, ainsi que les lignes incomplètes
- Remplit la valeur manquante de
riderpar"unassigned" - Affiche le total des frais connus, le nombre de livraisons sans frais et le tarif par kilomètre — total des frais divisé par la distance totale — calculé uniquement à partir des livraisons pour lesquelles le montant et la distance sont tous deux connus
Les trois dernières lignes affichées par votre programme doivent être exactement :
Fees collected (known): 540.0
Deliveries with no fee: 1
Fee per km: 13.21 from 5 of 8 deliveriesRédigez également, avant le code, une liste de décisions comme celle présentée dans « Avant d'écrire le code » : pour chacune des colonnes distance_km, fee et rider, que signifie un manque, et allez-vous le remplir, le supprimer ou le conserver ?
Tentez ensuite une expérience : calculez le tarif par kilomètre avec un simple dropna() au lieu de dropna(subset=...). Le chiffre change-t-il ? Combien de lignes a-t-il utilisées, et pourquoi ?
Solution
Le plan d'abord. La question comporte trois volets : un total, un décompte de manques et un ratio. L'examen de info() montrerait fee sous forme de str — une colonne de frais devrait être numérique, du texte s'y dissimule donc. Décisions par colonne :
rider— la livraison a eu lieu, le livreur n'a pas été consigné. Remplir avec"unassigned".fee— nous ignorons ce qui a été facturé. ConserverNaN; additionner les frais connus et compter les inconnus.distance_km— nous ignorons la distance parcourue. ConserverNaN; exclure ces lignes du ratio par kilomètre.
Pour le ratio, les deux nombres doivent être connus pour la même livraison — faute de quoi vous diviseriez les frais de sept livraisons par les distances de six livraisons.
Étape 1 — lire sans option particulière et observer, afin de repérer le marqueur :
import pandas as pd
raw = pd.read_csv("deliveries.csv")
print(raw.shape)
print(raw.dtypes)
print(raw["fee"].unique())
print(raw["rider"].unique())(8, 5)
order_id int64
branch str
distance_km float64
fee str
rider str
dtype: object
<StringArray>
['60', '?', '120', '40', '90', '110']
Length: 6, dtype: str
<StringArray>
['R1', 'R2', nan, '?', 'R3']
Length: 5, dtype: strfee est de type str en raison du ?. Et ? apparaît également comme livreur — le même marqueur, employé dans deux colonnes. distance_km est déjà de type float64 : ses manques sont des cellules vides, que pandas a reconnues de lui-même. Comme le marqueur est identique partout, na_values=["?"] lors de la lecture règle le problème dans les deux colonnes à la fois.
Le programme complet, deliveries_report.py :
import pandas as pd
deliveries = pd.read_csv("deliveries.csv", na_values=["?"])
print("Types after reading:")
print(deliveries.dtypes)
print()
print("Missing per column:")
print(deliveries.isna().sum())
print()
print("Incomplete rows:")
print(deliveries[deliveries.isna().any(axis=1)])
print()
deliveries["rider"] = deliveries["rider"].fillna("unassigned")
fee_total = deliveries["fee"].sum()
fee_missing = deliveries["fee"].isna().sum()
print("Fees collected (known):", fee_total)
print("Deliveries with no fee:", fee_missing)
both = deliveries.dropna(subset=["fee", "distance_km"])
per_km = both["fee"].sum() / both["distance_km"].sum()
print("Fee per km:", round(per_km, 2), "from", len(both), "of", len(deliveries), "deliveries")Types after reading:
order_id int64
branch str
distance_km float64
fee float64
rider str
dtype: object
Missing per column:
order_id 0
branch 0
distance_km 2
fee 1
rider 2
dtype: int64
Incomplete rows:
order_id branch distance_km fee rider
1 2002 north NaN 60.0 R2
2 2003 south 8.0 NaN R1
3 2004 east 12.0 120.0 NaN
5 2006 south 6.5 90.0 NaN
6 2007 east NaN 110.0 R3
Fees collected (known): 540.0
Deliveries with no fee: 1
Fee per km: 13.21 from 5 of 8 deliveriesVérifiez les chiffres à la main une fois, car c'est ainsi que l'on apprend à faire confiance à un total nettoyé. Frais connus : 60 + 60 + 120 + 40 + 90 + 110 + 60 = 540, sur sept livraisons — 2003 n'en a aucun. Pour le ratio, dropna(subset=["fee", "distance_km"]) élimine 2003 (pas de frais) ainsi que 2002 et 2007 (pas de distance). Demandez à pandas quelles lignes ont été conservées et à combien s'élève leur somme, plutôt que de vous fier au calcul mental :
print(both["order_id"].tolist())
print(both["fee"].sum(), both["distance_km"].sum())[2001, 2004, 2005, 2006, 2008]
370.0 28.0370 / 28.0 = 13.21, ce qui correspond au chiffre affiché par le programme. Remarquez ce que le ratio n'a pas fait : il n'a pas divisé les 540 issus de sept frais par les distances de six livraisons. Les deux nombres proviennent des cinq mêmes lignes, ce qui est la seule manière pour qu'un chiffre « par kilomètre » ait un sens.
Deux autres décisions méritent d'être soulignées. rider a été rempli avant le calcul, mais n'a jamais influencé celui-ci — subset= n'a porté que sur fee et distance_km. Et le total des frais est libellé « (known) », le décompte des frais manquants étant affiché juste en dessous, afin que personne ne prenne 540 pour l'intégralité de ce qui a été collecté.
L'expérience. Un simple dropna() supprime également 2004 et 2006 — dont les frais et la distance étaient tous deux valides — parce que leur livreur était manquant. Si vous l'exécutez sur le tableau avant de remplir rider :
deliveries = pd.read_csv("deliveries.csv", na_values=["?"])
strict = deliveries.dropna()
print(strict["order_id"].tolist())
print(round(strict["fee"].sum() / strict["distance_km"].sum(), 2))[2001, 2005, 2008]
16.84Seules trois livraisons subsistent, et le ratio bondit de 13.21 à 16.84 — non pas parce qu'un quelconque tarif a changé, mais parce que les deux trajets les plus longs (12 et 6.5 km), qui se trouvent être moins chers au kilomètre, ont été écartés pour un nom manquant. La colonne des livreurs n'a rien à voir avec la question posée, elle ne devrait donc pas décider des lignes qui comptent. C'est précisément à cela que sert subset=.
Étape 6 sur 6
Défi — le quiz du chapitre
Dix questions, de la plus facile à la plus difficile. Les dernières sont difficiles exprès.
Connectez-vous pour faire le quiz