Fehlende Daten — Lücken finden und entscheiden, was sie bedeuten
Jede Lücke in einer Tabelle finden, auch solche, die als Text wie - oder ? versteckt sind, verstehen, wie NaN Summen, Mittelwerte und Typen verändert, und Spalte für Spalte gezielt entscheiden, ob Lücken gelöscht, gefüllt oder behalten werden.
Wie möchtest du lernen?
Das Problem, das wir lösen
Die Bestellungen des Geschäfts für die erste Märzwoche wurden in drei Filialen von Hand eingegeben und als orders_raw.csv exportiert. Die Inhaberin möchte bis zum Abend zwei Zahlen haben: Wie viele Artikel wurden verkauft und wie viel Geld wurde eingenommen?
Du liest die Datei ein und addierst aus Gewohnheit aus den früheren Kapiteln zuerst die Preisspalte, nur um die Größenordnung zu sehen:
import pandas as pd
orders = pd.read_csv("orders_raw.csv")
print(orders["price"].sum())15.060.0850.08.0-15.0120.0Das ist keine Zahl. Es sind alle Preise als Text aneinandergehängt — und pandas hat keinen Fehler gemeldet, um dich darauf hinzuweisen. Nun die Mengenspalte:
print(orders["quantity"].sum())
print(orders["quantity"].mean())76.0
10.857142857142858Das sieht in Ordnung aus — und genau das ist noch schlimmer. Es gibt acht Bestellungen in der Datei, doch 76 / 8 ergibt 9.5, nicht 10.86. Eine Menge ist leer, und pandas hat sie stillschweigend sowohl bei der Gesamtsumme als auch beim Durchschnitt weggelassen. Auch das hat dir niemand gesagt.
Die Datei weist also Lücken auf, und diese Lücken treten in zwei Varianten auf. Einige sind sichtbar: eine leere Zelle, die pandas in eine spezielle „Fehlend“-Markierung umwandelt und dann bei arithmetischen Operationen stillschweigend überspringt. Andere sind versteckt: Jemand hat - eingegeben, anstatt die Zelle leer zu lassen, pandas hat das für bare Münze genommen, und die gesamte Preisspalte wurde zu Text.
Keine der beiden Varianten löst einen Fehler aus. Beide verändern das Ergebnis. In diesem Kapitel geht es darum, jede Lücke zu finden, zu verstehen, was pandas damit macht, und dann eine bewusste Entscheidung zu treffen — löschen, füllen oder behalten —, anstatt diese Entscheidung den Standardeinstellungen zu überlassen.
Am Ende dieses Kapitels kannst du
- erklären, was
NaNist, warumNaN == NaNgleichFalseist und warum eine Ganzzahlspalte zu Dezimalzahlen wird, wenn eine Zelle leer ist - fehlende Werte pro Spalte mit
isna().sum()zählen und die unvollständigen Zeilen herausfiltern - versteckte Markierungen wie
-oder?mitna_values=oderpd.to_numeric(errors="coerce")in echte fehlende Werte umwandeln - vorhersagen, wie
sum,mean,countund Spaltenarithmetik fehlende Werte behandeln - fehlende Werte mit
dropna()entfernen und dabeisubset=sowiehow=nutzen, um nur das zu entfernen, was du wirklich beabsichtigst - fehlende Werte mit
fillna()ersetzen — durch einen einzelnen Wert, einen unterschiedlichen Wert pro Spalte oder einen statistischen Kennwert - Spalte für Spalte entscheiden, ob gelöscht, gefüllt oder behalten werden soll — und begründen, warum
- die
inplace=True-Falle vermeiden, die unter pandas 3 wirkungslos bleibt
Voraussetzungen: Zeilen filtern.
Erstelle zuerst die Datei
Erstelle in deinem Projektordner die Datei orders_raw.csv mit genau diesen Zeilen. Die Lücken sind beabsichtigt — sieh dir die dritte, fünfte und siebte Zeile genau an:
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.0Es sind dieselben acht Bestellungen wie in orders.csv aus den früheren Kapiteln, allerdings mit vier Fehlern: Bestellung 1002 hat keine Menge, Bestellung 1004 hat keine Filiale und ihr Preis lautet N/A, und Bestellung 1006 hat - als Preis.
Bevor du den Code schreibst
Fehlende Daten sind das eine Thema, bei dem es ausgesprochen gefährlich ist, sofort Code zu schreiben: dropna() und fillna() laufen beide ohne Murren durch und beide verändern dein Ergebnis. Die Arbeit beginnt daher mit Hinsehen und Entscheiden.
1. Formuliere die Frage in einem Satz. „Wie hoch war die insgesamt verkaufte Stückzahl und der Gesamtumsatz (Menge × Preis) für die Woche — und wie viele Bestellungen konnten nicht gezählt werden?“ Der letzte Teil ist entscheidend. Ein Gesamtwert, der stillschweigend zwei Bestellungen auslässt, ist nicht dieselbe Antwort wie ein Gesamtwert, der angibt: „6 von 8 Bestellungen gezählt“.
2. Schau dir zuerst an, was angekommen ist. shape und info() zusammen informieren dich über alle Spalten auf einmal:
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 bytesGleiche die Ausgabe Spalte für Spalte mit deinen Erwartungen ab:
branch— erwartet wurden 8 non-nullstr, angekommen sind 7 non-null. Eine Filiale fehlt.quantity— erwartet wurden 8 non-nullint64, angekommen sind 7 non-nullfloat64. Eine Menge fehlt, und genau deshalb wurde darausfloat64(Erklärung folgt unten).price— erwartet wurden 8 non-nullfloat64, angekommen sind 7 non-nullstr. Ein Preis fehlt und es ist etwas Text in die Spalte geraten.
Zwei Dinge in dieser Liste sind eher Hinweise als Gewissheiten. Eine Ganzzahlspalte mit float64 bedeutet fast immer eine Lücke. Eine Zahlenspalte mit str bedeutet immer, dass Text daruntergemischt ist — und Text in einer Zahlenspalte ist meist eine versteckte „Fehlend“-Markierung.
3. Skizziere die gewünschte Ausgabe. Etwa so:
Missing values per column: branch 1, quantity 1, price 2
Orders counted for revenue: 6 of 8
Total quantity (known): 76
Total revenue (counted): ...Beachte, dass sowohl die Lücken als auch die Gesamtsummen aufgeführt sind. Wer den Bericht liest, sollte wissen, dass der Umsatz eine Untergrenze darstellt.
4. Entscheide dich für das passende Werkzeug für jede Lücke — bevor du den Code schreibst. Frage dich für jede Spalte: Was bedeutet eine leere Zelle hier, und lässt sich die Frage auch ohne sie beantworten?
branch— eine Lücke bedeutet, dass die Bestellung stattgefunden hat, die Filiale jedoch nicht erfasst wurde. Entscheidung: Füllen mit"Unknown", denn die Bestellung existiert real und ihr Betrag zählt weiterhin.quantity— eine Lücke bedeutet, dass wir nicht wissen, wie viele Artikel verkauft wurden. Entscheidung: Als fehlend behalten und die Zeile beim Umsatz ausschließen, denn eine erfundene Zahl (0? der Durchschnitt?) würde Verkäufe vortäuschen.price— eine Lücke bedeutet, dass wir den Verkaufspreis nicht kennen. Entscheidung: Aus demselben Grund als fehlend behalten und die Zeile beim Umsatz ausschließen — und festhalten, wie viele Zeilen ausgeschlossen wurden.
5. Lege fest, was du im Nachhinein überprüfen wirst. Gib nach jedem Schritt, der Werte entfernt oder ersetzt, erneut die Anzahl der fehlenden Werte sowie die Zeilen- und Spaltenanzahl (shape) aus. Wenn ein dropna() dich von 8 Zeilen auf 2 reduziert, möchtest du das sofort sehen — und nicht erst, nachdem der Bericht verschickt wurde.
Im restlichen Kapitel lernst du jedes Werkzeug der Reihe nach kennen, und am Ende fassen wir diesen Plan in einem einzigen Programm zusammen.
Wie „fehlend“ aussieht: NaN
Wenn pandas eine leere Zelle vorfindet, speichert es keinen leeren String und keine Null. Es speichert einen speziellen Gleitkommawert namens NaN — „Not a Number“. Du kannst selbst einen solchen erzeugen, indem du None in eine Series einfügst:
import pandas as pd
quantity = pd.Series([12, None, 2])
print(quantity)0 12.0
1 NaN
2 2.0
dtype: float64Sieh dir den dtype an. Du hast zwei ganze Zahlen und eine Lücke übergeben und float64 mit 12.0 und 2.0 zurückerhalten. Das ist der Grund für den Hinweis in info(): NaN ist ein Float-Wert, und eine klassische Integer-Spalte hat keine Möglichkeit, ihn aufzunehmen. Sobald also eine einzige Zelle fehlt, wandelt pandas die gesamte Spalte in float64 um, um Platz für NaN zu schaffen. Aus diesem Grund wurde quantity in unserer Datei als 12.0, 2.0 usw. ausgegeben.
Textspalten verhalten sich anders. Eine str-Spalte kann einen fehlenden Wert aufnehmen, ohne ihren Typ zu ändern:
branch = pd.Series(["north", None, "east"])
print(branch)0 north
1 NaN
2 east
dtype: strDie Ausgabe zeigt weiterhin NaN, und der Dtype bleibt str. In pandas 3 wirst du NaN also sowohl in Zahlen- als auch in Textspalten antreffen, und dieselben Werkzeuge finden es in beiden.
NaN ist nichts gleich — nicht einmal sich selbst
Das ist die verblüffendste Tatsache an fehlenden Werten, und sie macht die naheliegendste Suchmethode zunichte:
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 bedeutet „ein Wert, den wir nicht kennen“. Zwei unbekannte Werte sind bekanntermaßen nicht gleich, weshalb der Vergleich False liefert — jedes Mal. Das macht == np.nan zu einem Test, der niemals auf irgendetwas zutrifft: Die Filialspalte weist eine Lücke auf, und die Zählung liefert 0. Frage stattdessen pandas direkt.
Suche niemals mit==nach fehlenden Werten.== np.nanläuft ohne Fehler durch und findet niemals etwas, sodass eine Spalte voller Lücken vollständig aussieht. Verwendeisna().
Fehlende Werte finden: isna()
isna() stellt die Frage, die == nicht stellen kann: „Fehlt diese Zelle?“ Auf eine Spalte angewendet, liefert es eine Spalte aus True/False zurück — eine Maske, genau wie im Kapitel über das Filtern:
print(orders["branch"].isna())0 False
1 False
2 False
3 True
4 False
5 False
6 False
7 False
Name: branch, dtype: boolAuf den gesamten DataFrame angewendet, geschieht dasselbe für jede Zelle. Allerdings möchte man selten acht Zeilen voller True/False durchlesen. Man möchte eine Zahl, und da True als 1 zählt, liefert .sum() genau das:
print(orders.isna().sum())order_id 0
date 0
branch 1
product 0
category 0
quantity 1
price 1
dtype: int64Das ist die Zeile, die man bei jeder neuen Datei direkt nach info() ausführen sollte. Es sind dieselben Informationen wie beim Non-Null Count, nur umgedreht, um die Lücken direkt zu zählen.
Aber sieh dir price an: Dort steht 1. Wir wissen jedoch, dass zwei Preise unbrauchbar sind — N/A und -. Behalte das im Hinterkopf; wir kommen im nächsten Abschnitt darauf zurück.
Welche Zeilen sind unvollständig?
Eine Zählung verrät dir, wie viele es sind; um zu sehen, welche es sind, nutzt du die Maske als Filter. isna().any(axis=1) fragt für jede Zeile: „Fehlt in dieser Zeile irgendetwas?“:
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 bedeutet „zeilenweise quer prüfen“ statt spaltenweise nach unten. Und beachte, wer nicht in der Liste steht: Bestellung 1006, deren Preis - lautet. Soweit pandas weiß, ist diese Zeile vollständig. Behalte diesen Gedanken im Kopf. Ein Blick auf die tatsächlichen Zeilen ist die zusätzliche Zeile Code wert: So bemerkst du beispielsweise, dass jede fehlende Menge aus derselben Filiale stammt — eine Frage, die man eher den Kollegen in dieser Filiale stellt als pandas.
Als Anteil, nicht als Anzahl
Bei acht Zeilen sind Anzahlen völlig in Ordnung. Bei achtzigtausend Zeilen sagt „312 fehlend“ wenig aus, solange man nicht weiß, dass es 0,4 % sind. .mean() auf einer True/False-Spalte ergibt den Anteil an 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: float64Eine von acht Zellen bei branch, quantity und price fehlt — jeweils 12,5 %. Eine Spalte, die zu 90 % leer ist, und eine Spalte, die zu 0,1 % leer ist, verlangen völlig unterschiedliche Entscheidungen, und so unterscheidest du sie.
Versteckte fehlende Werte: Wenn die Lücke als Text vorliegt
isna() hat einen fehlenden Preis gezählt, aber zwei Preise sind unbrauchbar. Der zweite ist das - in Bestellung 1006. Für pandas ist - nicht fehlend — es ist ein ganz normaler Text. Und ein einzelnes Stück Text in einer Zahlenspalte reicht aus, um die gesamte Spalte zu str zu machen, genau wie info() es gezeigt hat.
So findest du heraus, was tatsächlich in einer Spalte steht:
print(orders["price"].unique())<StringArray>
['15.0', '60.0', '850.0', nan, '8.0', '-', '120.0']
Length: 7, dtype: strJeder Wert steht in Anführungszeichen — alle sind Text, einschließlich '15.0'. Und es gibt zwei Arten von Lücken nebeneinander: nan, das pandas erkannt hat, und '-', das es nicht erkannt hat.
Warum wurde N/A zu nan, - aber nicht? Weil read_csv eine eingebaute Liste von Strings mitbringt, die es als fehlend behandelt: eine leere Zelle, NA, N/A, n/a, NaN, null, None und einige andere. -, ?, missing, 0 und unknown stehen nicht auf dieser Liste. Was auch immer die Person bei der Dateneingabe für „weiß nicht“ verwendet hat, entscheidet darüber, welche Art von Lücke du vorfindest.
Es gibt zwei Wege, die versteckten Lücken in echte NaN umzuwandeln.
Lösung 1: read_csv die Markierung mitteilen — na_values=
Wenn du die Markierung kennst, ist die sauberste Lösung direkt beim Einlesen. na_values= fügt deine Strings der eingebauten Liste hinzu:
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: int64Vergleiche das mit der vorherigen Zählung. branch und quantity sind unverändert — na_values= ergänzt die Liste, anstatt sie zu ersetzen, sodass leere Zellen und N/A weiterhin erkannt werden. Geändert hat sich price: Die Spalte ist jetzt float64 und meldet 2 fehlende Werte — N/A und - sind nun beide echte NaN. Die Spalte ist wieder numerisch, arithmetische Operationen darauf bedeuten also echte Berechnungen.
Beachte, dass na_values=["-"] für jede Spalte gilt. Hier ist genau das erwünscht. Könnte ein - an anderer Stelle ein gültiger Wert sein — etwa ein Produktcode —, übergibst du stattdessen ein Dictionary wie na_values={"price": ["-"]}, und nur die Spalte price ist betroffen.
Lösung 2: Nachträglich konvertieren — pd.to_numeric(errors="coerce")
Manchmal weißt du nicht im Voraus, welche merkwürdigen Strings in einer Spalte stehen, oder die Datei wurde durch den Code eines anderen eingelesen. Dann konvertierst du die Spalte nach dem Einlesen. Sieh dir zuerst an, was eine einfache Konvertierung bewirkt:
raw = pd.read_csv("orders_raw.csv")
price = pd.to_numeric(raw["price"])ValueError: Unable to parse string "-" at position 5Dieser Fehler ist nützlich: Er nennt den problematischen Wert und seine Position. Das Hinzufügen von errors="coerce" besagt: „Alles, was du nicht in eine Zahl umwandeln kannst, machst du zu NaN“:
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: float64Um die Tabelle tatsächlich zu ändern, weist du das Ergebnis wieder der Spalte zu — to_numeric gibt eine neue Series zurück und lässt raw unberührt:
raw["price"] = pd.to_numeric(raw["price"], errors="coerce")
print(raw["price"].dtype, raw["price"].isna().sum())float64 2Welche Lösung wählen?
errors="coerce" ist wirkungsvoll und radikal. Es wandelt alles Nicht-Numerische in NaN um — einschließlich eines echten Preises, der versehentlich als 1O0 (Buchstabe O) oder 1,200 mit Tausendertrennzeichen eingetippt wurde. Das sind keine fehlenden Werte; es sind Werte mit Tippfehlern, und das Erzwingen wirft diese Informationen stillschweigend weg.
Die Gewohnheit lautet daher: Vorher zählen, nachher zählen und den Unterschied erklären.
- Du kennst die Markierung (
-,?,missing): Verwendena_values=inread_csv. Das ist präzise — nur dieser bestimmte String wird als fehlend gewertet. - Unbekannter Datenmüll, und die Spalte muss numerisch sein: Verwende
pd.to_numeric(errors="coerce"). Das erfasst alles — überprüfe also, was erfasst wurde. - Einige Werte sind Tippfehler, die du korrigieren kannst: Sieh dir zuerst
unique()an, behebe die Fehler und konvertiere erst dann. Ein Erzwingen würde echte Daten zerstören.
Vor dem Erzwingen zeigte raw["price"].unique() genau einen nicht-numerischen String ('-') plus ein nan. Nach dem Erzwingen gibt es genau 2 NaN. Die Zahlen stimmen überein, es ging also nichts anderes verloren. Diese Prüfung macht den gesamten Unterschied zwischen dem Bereinigen von Daten und deren Beschädigung aus.
Von hier an verwenden wir die Version, die mit na_values=["-"] eingelesen wurde.
Wie Berechnungen NaN behandeln
Nachdem die Lücken nun echte NaN sind, musst du wissen, was pandas damit anstellt — denn es hält nicht an, um nachzufragen.
Spaltenzusammenfassungen überspringen 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.857142857142858Drei Verhaltensweisen, die man sich merken sollte:
lenzählt Zeilen;count()zählt Werte. Die Differenz zwischen beiden ist die Anzahl der Lücken. Das ist derselbe Gedanke wie beimNon-Null Countininfo().sum()überspringtNaN. Es addiert die sieben bekannten Mengen. Das ist naheliegend — bedeutet aber, dass „76“ die Gesamtsumme der bekannten Mengen ist, nicht aller Bestellungen.mean()teilt durchcount(), nicht durchlen. Der Durchschnitt bezieht sich auf die sieben bekannten Werte. Das ist meist genau das, was du möchtest; die Lücke als 0 zu behandeln, würde den Durchschnitt grundlos nach unten ziehen.
Wenn du lieber darüber informiert werden möchtest, dass etwas fehlt, lässt skipna=False das Ergebnis zu NaN werden, sobald auch nur ein einziger Wert fehlt:
print(q.sum(skipna=False))nanArithmetik zwischen Spalten verbreitet NaN
Zusammenfassungen überspringen Lücken; Arithmetik verbreitet sie. Alles, was mit NaN kombiniert wird, ergibt NaN, denn „7 mal ein unbekannter Preis“ ist selbst unbekannt:
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 8Nimm beide Verhaltensweisen zusammen, und du erkennst die Falle vom Anfang des Kapitels: revenue.sum() gab ohne jede Warnung 2900.0 aus — aber drei der acht Bestellungen haben überhaupt nichts beigetragen. Bei Bestellung 1002 fehlt die Menge, bei 1004 und 1006 fehlt der Preis. Die Gesamtsumme ist nur dann ehrlich, wenn du „5 von 8 Bestellungen“ dazusagst. Genau deshalb verlangte der Plan eine Zählung neben jeder Gesamtsumme.
Fehlende Werte entfernen: dropna()
dropna() entfernt Zeilen, die NaN enthalten. Ohne Argumente entfernt es jede Zeile mit irgendeiner Lücke in irgendeiner Spalte:
print(orders.shape)
print(orders.dropna().shape)(8, 7)
(5, 7)Drei von acht Zeilen sind weg — und Bestellung 1004 flog heraus, weil ihre Filiale fehlte, obwohl sie für eine Frage zur Menge eine vollkommen einwandfreie Zeile war. Das ist das Problem mit einem reinen dropna(): Es beantwortet „entferne alles Unvollkommene“, was selten die eigentliche Frage ist.
subset= — nur die Spalten, die die Frage benötigt
Gib an, welche Spalten zwingend vorhanden sein müssen:
known_qty = orders.dropna(subset=["quantity"])
print(known_qty.shape)
print(known_qty["quantity"].sum())(7, 7)
76.0Nur Bestellung 1002 wird entfernt — die einzige Zeile, bei der die Menge wirklich unbekannt ist. Für den Umsatz werden beide Spalten benötigt:
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.0Achte auf den Index: 0, 2, 4, 6, 7. dropna() behält die ursprünglichen Labels bei, genau wie beim Filtern, sodass du eine Zeile immer bis zur Datei zurückverfolgen kannst. Falls du wieder 0, 1, 2… benötigst, hänge .reset_index(drop=True) an.
how="all" — nur Zeilen, die vollkommen leer sind
Exportierte Tabellenkalkulationen enden oft mit ein paar Zeilen, die in jeder Spalte leer sind. how="all" entfernt genau diese und behält jede Zeile mit auch nur einem einzigen Wert:
print(orders.dropna(how="all").shape)(8, 7)Hier wurde nichts entfernt — keine unserer Zeilen ist vollkommen leer —, was genau das ist, was du bestätigen möchtest.
dropna() gibt eine neue Tabelle zurück
Wie die meisten pandas-Methoden verändert dropna() das Objekt orders nicht; es liefert dir einen neuen DataFrame. Wenn du orders.dropna() als einzelne Zeile schreibst, geschieht mit orders überhaupt nichts. Weise das Ergebnis zu — unter einem neuen Namen, falls du die vollständige Tabelle noch brauchst, was in einem Bericht wie unserem der Fall ist (um anzugeben, wie viele Zeilen verworfen wurden).
Fehlende Werte ersetzen: fillna()
Manchmal gibt es für eine Lücke einen sinnvollen Ersatz. fillna() setzt einen Wert dorthin, wo vorher NaN stand.
Ein Wert für eine Spalte
Die fehlende Filiale ist der eindeutigste Fall. Die Bestellung fand statt und das Geld ist real; wir wissen lediglich nicht, welche Filiale sie abgewickelt hat. Das Füllen mit einer Bezeichnung erhält die Zeile und macht die Lücke bei jeder späteren Zählung nach Filialen sichtbar:
orders["branch"] = orders["branch"].fillna("Unknown")
print(orders["branch"].tolist())['north', 'south', 'north', 'Unknown', 'north', 'south', 'east', 'north']Der Aufbau dieser Zeile ist wichtig: Spalte nehmen, fillna anwenden und wieder derselben Spalte zuweisen. fillna gibt eine neue Series zurück; ohne das vorangestellte orders["branch"] = wird nichts gespeichert.
Unterschiedliche Werte für verschiedene Spalten: ein Dictionary
Übergib ein Dictionary aus {column: value}, um mehrere Spalten auf einmal zu füllen, jede mit ihrem eigenen Wert. Nicht genannte Spalten bleiben unberührt:
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 hat weiterhin seine zwei Lücken, weil wir es nicht erwähnt haben. Das ist der Vorteil der Dictionary-Form gegenüber orders.fillna(0), was jede Spalte, einschließlich der Preise, mit 0 füllen würde — und ein Preis von 0 wäre eine geschenkte Tasche.
Ersetzen verändert das Ergebnis
Das Füllen der Menge mit 0 sah harmlos aus. Vergleiche die Mittelwerte:
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.375Drei verschiedene Durchschnitte aus einer einzigen Spalte, und der einzige Unterschied liegt darin, was eine unbekannte Menge deiner Entscheidung nach bedeutet:
- 0 besagt: „Bei dieser Bestellung wurde nichts verkauft“. Für Bestellung 1002, eine Notizbuchbestellung, bei der ganz sicher etwas verkauft wurde, ist das falsch und zieht den Durchschnitt nach unten.
- Der Median (hier 7) besagt: „Nimm eine typische Bestellung an“. Er hält den Durchschnitt ungefähr stabil, aber die Gesamtsumme enthält nun 7 Notizbücher, die niemand gezählt hat.
- Überspringen besagt: „Wir wissen es nicht, also lassen wir es weg“. Der Durchschnitt bezieht sich auf die sieben Bestellungen, die wir tatsächlich kennen.
Keine dieser Varianten ist allgemein „richtig“. Jede beantwortet eine etwas andere Frage. Genau deshalb gehört die Entscheidung schriftlich in deinen Plan und nicht in irgendeine Standardeinstellung, nach der du spontan gegriffen hast.
Eine Faustregel, die fast immer funktioniert:
- Eine Bezeichnung (Filiale, Kategorie, Name): Mit
"Unknown"oder Ähnlichem füllen. Die Zeile bleibt erhalten, und die Lücke bleibt als eigene Gruppe sichtbar. - Eine Zählung, bei der leer wirklich null bedeutet (Retouren, Reklamationen): Mit
0füllen — aber nur, wenn die Datenquelle mit einer Leerstelle auch wirklich null meint. - Ein Messwert oder Geldbetrag (verkaufte Menge, Preis, Note):
NaNbeibehalten, aus der Berechnung ausschließen und angeben, wie viele Zeilen ausgeschlossen wurden. Erfundene Zahlen werden zu erfundenen Summen. - Eine Spalte, die die Fragestellung benötigt und die größtenteils vorhanden ist:
dropna(subset=[...])nur für diese spezifische Frage. Ein kleiner Verlust und eine ehrliche Antwort.
Ganze Zahlen zurückerhalten
Nach dem Füllen weist quantity keine Lücken mehr auf, ist aber immer noch float64 — pandas stellt den ursprünglichen Typ nicht von selbst wieder her. Sobald kein NaN mehr vorhanden ist, funktioniert astype(int):
filled["quantity"] = filled["quantity"].astype(int)
print(filled["quantity"].tolist())
print(filled["quantity"].dtype)[12, 0, 2, 7, 30, 1, 20, 4]
int64Versuche das vor dem Füllen, und es schlägt fehl, weil es keine Ganzzahl gibt, die „unbekannt“ bedeutet — siehe „Wenn etwas schiefgeht“ weiter unten.
Ein vollständiges Beispiel
clean_orders.py überführt den Plan aus „Bevor du den Code schreibst“ in ein einziges Programm: Mit der bekannten Markierung einlesen, jede Lücke melden, die Filiale füllen, Zeilen ausschließen, die nicht gezählt werden können, und die Gesamtsummen zusammen mit der Anzahl der berücksichtigten Bestellungen ausgeben.
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 NaNWarum es so geschrieben ist:
na_values=["-"]steht direkt in der ersten Zeile. Die Markierung wurde einmalig durch den Blick aufunique()gefunden. Wenn man sie inread_csvangibt, ist die Spalte von Anfang an numerisch, und jeder, der das Programm liest, sieht sofort, welche Markierung die Datei verwendet.missing[missing > 0]gibt nur die Spalten aus, die tatsächlich Lücken aufweisen. Bei einer Datei mit vierzig Spalten ist das der Unterschied zwischen einem lesbaren Bericht und einer Wand aus Nullen.- Die Filiale wird gefüllt, die Zahlen nicht. Das ist die Entscheidungsliste aus dem Plan, ausgedrückt als Code. Eine lesende Person mag anderer Meinung sein, sieht die Entscheidung jedoch klar vor sich.
dropna(subset=...)benennt genau die beiden Spalten, die der Umsatz benötigt, sodass Bestellung 1004 wegen ihres fehlenden Preises ausgeschlossen wird und nicht wegen ihrer fehlenden Filiale.- Die ausgelassenen Zeilen werden ausgegeben, nicht nur gezählt. „3 Bestellungen nicht gezählt“ provoziert die Frage: „Welche denn?“ — beantworte sie im Bericht. Diese drei Bestellungen bilden eine Liste, die jemand tatsächlich gezielt überprüfen und korrigieren kann.
- „Total quantity (known)“ und „(counted)“ machen deutlich, was die Zahlen darstellen. Beide sind Untergrenzen; die Bezeichnungen verhindern, dass jemand sie als vollständig missversteht.
Wenn etwas schiefgeht
TypeError: Cannot perform reduction 'mean' with string dtype Du hast den Mittelwert einer Spalte angefordert, die pandas als Text eingelesen hat — meistens, weil eine Zelle eine Markierung wie -, ? oder missing enthält. Führe df["col"].unique() aus, um den Übeltäter zu finden, und lies die Datei entweder mit na_values=["-"] erneut ein oder konvertiere mit 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() hat eine lange Ziffernfolge zurückgegeben, wie 15.060.0850.0... Gleiche Ursache, schlimmeres Symptom: Bei einer Textspalte fügt sum() die Strings aneinander, anstatt Zahlen zu addieren, und löst keinerlei Fehler aus. Jede Summe, die so aussieht, stammt aus einer str-Spalte. Überprüfe df.dtypes, bevor du irgendetwas aufsummierst.
ValueError: Unable to parse string "-" at position 5 pd.to_numeric ist auf Text gestoßen, den es nicht umwandeln konnte, und hat dir genau mitgeteilt, was und wo. Füge entweder errors="coerce" hinzu (nachdem du mit unique() geprüft hast, dass die fraglichen Werte wirklich „Unbekannt“-Markierungen und keine Tippfehler sind), oder korrigiere zuerst die Tippfehler.
(df["col"] == np.nan).sum() liefert 0, obwohl Lücken vorhanden sind NaN ist niemals irgendetwas gleich, auch nicht sich selbst, daher passt == np.nan auf keine einzige Zelle. Verwende df["col"].isna().
IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer Du hast astype(int) auf einer Spalte aufgerufen, die noch NaN enthält. Es gibt keine Ganzzahl mit der Bedeutung „unbekannt“. Fülle oder entferne die Lücken zuerst und konvertiere erst danach:
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')(Die fehlenden Leerzeichen in „integer.Replace“ und „typethat“ stammen aus der Originalmeldung von pandas.) Die Meldung erwähnt auch 'Int64' mit großem I: ein separater Integer-Typ von pandas, der fehlende Werte aufnehmen kann. Er ist nützlich, aber das vorherige Füllen oder Entfernen ist die einfachere Lösung und diejenige, zu der man zuerst greifen sollte.
ChainedAssignmentError — und die Lücke ist immer noch da Du hast die Schreibweise mit inplace=True verwendet, die man in vielen älteren Anleitungen findet:
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.htmlDie Anzahl ist immer noch 1: Es wurde nichts gefüllt. In pandas 3 verhält sich orders["quantity"] immer wie eine eigene Kopie (Copy-on-Write). Wenn du es „in place“ füllst, wird diese Kopie gefüllt, die anschließend sofort verworfen wird. Ältere pandas-Versionen ließen das manchmal funktionieren, weshalb dieses Muster im Internet allgegenwärtig ist. Schreibe stattdessen die Zuweisung explizit aus — das funktioniert in jeder Version und sagt unmissverständlich, was sich ändert:
orders["quantity"] = orders["quantity"].fillna(0)
print(orders["quantity"].isna().sum())0dropna() hat viel mehr Zeilen entfernt als erwartet Ein reines dropna() entfernt eine Zeile bei einer Lücke in irgendeiner Spalte — einschließlich einer fast leeren Spalte wie notes oder discount, die du überhaupt nicht verwendet hast. Gib df.isna().sum() aus, um die Spalte mit vielen Lücken zu identifizieren, und verwende dann dropna(subset=[...]) nur mit den Spalten, die deine Frage benötigt.
TypeError: can't multiply sequence by non-int of type 'float' Du hast eine Zahlenspalte mit einer Textspalte multipliziert (quantity * price, während price den Typ str hat). Die Lösung ist dieselbe wie beim ersten Fehler: Wandle price zuerst in einen numerischen Typ um.
Schritt 4 von 6 — Vorhersagen
Prüfe dein Verständnis
Eine Menge ist leer. Was wird ausgegeben?
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
Das Ziel ist es, die Bestellungen ohne Preis zu finden. Zwei Preise fehlen. Was wird ausgegeben?
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` ist nichts gleich, nicht einmal `NaN`, daher findet `==` es niemals
- B1
- CEin `TypeError`: Man kann nicht mit `nan` vergleichen
- D2
Die Preisspalte wurde als str eingelesen, weil eine Zelle - und eine andere N/A enthält. Welche Zeile wandelt sie in Zahlen um, sodass beide Lücken zu NaN werden?
- 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()
Zum Antworten brauchst du ein Konto
Melde dich an, um deine Antworten zu prüfen
Die Fragen stehen oben, und sie im Kopf durchzuarbeiten ist der wichtige Teil. Melde dich an, um die Antworten, die Erklärungen und die dreistufigen Hinweise zu sehen.
Jetzt bist du dran
Das Geschäft bietet jetzt auch Lieferungen nach Hause an, und das Fahrtenbuch der Fahrer für die erste Woche kommt als deliveries.csv an. Erstelle die Datei genau so:
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,R3Die Inhaberin fragt: Wie viel wurde an Liefergebühren eingenommen, bei wie vielen Lieferungen fehlt die Gebühr und wie hoch ist die durchschnittliche Gebühr pro Kilometer?
Bevor du Code schreibst, sieh dir die Datei mit unique() an und finde heraus, wie die versteckte Markierung lautet. Schreibe dann deliveries_report.py, das:
- die Datei so einliest, dass die versteckte Markierung in jeder Spalte, in der sie vorkommt, zu einem echten fehlenden Wert wird
- die Anzahl der fehlenden Werte pro Spalte sowie die unvollständigen Zeilen ausgibt
- die fehlenden Werte in
ridermit"unassigned"füllt - die Gesamtsumme der bekannten Gebühren, die Anzahl der Lieferungen ohne Gebühr und die Gebühr pro Kilometer ausgibt — Gesamteinnahmen geteilt durch Gesamtdistanz —, berechnet ausschließlich aus Lieferungen, bei denen sowohl Gebühr als auch Distanz bekannt sind
Die letzten drei Zeilen, die dein Programm ausgibt, sollten exakt so lauten:
Fees collected (known): 540.0
Deliveries with no fee: 1
Fee per km: 13.21 from 5 of 8 deliveriesSchreibe vor dem Code außerdem eine Entscheidungsliste auf, wie im Abschnitt „Bevor du den Code schreibst“: Was bedeutet eine Lücke jeweils bei distance_km, fee und rider, und wirst du sie füllen, löschen oder behalten?
Probiere dann ein Experiment aus: Berechne die Gebühr pro Kilometer mit einem reinen dropna() anstelle von dropna(subset=...). Ändert sich die Zahl? Wie viele Zeilen wurden verwendet und warum?
Lösung
Zuerst der Plan. Die Frage besteht aus drei Teilen: einer Summe, einer Zählung von Lücken und einem Verhältnis. Ein Blick auf info() würde fee als str zeigen — eine Gebührenspalte sollte aus Zahlen bestehen, also versteckt sich Text darin. Entscheidungen pro Spalte:
rider— die Lieferung fand statt, der Fahrer wurde nicht erfasst. Füllen mit"unassigned".fee— wir wissen nicht, was berechnet wurde.NaNbeibehalten; die bekannten Gebühren addieren und die unbekannten zählen.distance_km— wir kennen die Entfernung nicht.NaNbeibehalten; diese Zeilen beim Verhältnis pro Kilometer ausschließen.
Für das Verhältnis müssen beide Zahlen für dieselbe Lieferung bekannt sein — andernfalls würdest du die Gebühren von sieben Lieferungen durch die Distanzen von sechs Lieferungen teilen.
Schritt 1 — einfach einlesen und ansehen, um die Markierung zu finden:
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 ist wegen des ? vom Typ str. Und ? taucht auch bei den Fahrern auf — dieselbe Markierung, in zwei Spalten verwendet. distance_km ist bereits float64: Seine Lücken sind leere Zellen, die pandas von selbst erkannt hat. Da die Markierung überall identisch ist, behebt na_values=["?"] beim Einlesen beide Spalten auf einmal.
Das vollständige Programm, 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 deliveriesÜberprüfe die Zahlen einmal von Hand, denn so lernt man, einer bereinigten Summe zu vertrauen. Bekannte Gebühren: 60 + 60 + 120 + 40 + 90 + 110 + 60 = 540, aus sieben Lieferungen — 2003 hat keine. Für das Verhältnis verwirft dropna(subset=["fee", "distance_km"]) die Lieferung 2003 (keine Gebühr) sowie 2002 und 2007 (keine Distanz). Frage pandas, welche Zeilen übrig geblieben sind und was sie in Summe ergeben, anstatt dich auf Kopfrechnen zu verlassen:
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, genau die Zahl, die das Programm ausgegeben hat. Beachte, was bei der Verhältnisberechnung nicht getan wurde: Es wurden nicht die 540 aus sieben Gebühren durch die Distanzen von sechs Lieferungen geteilt. Beide Zahlen stammen aus denselben fünf Zeilen — nur so ergibt ein Wert „pro Kilometer“ überhaupt einen Sinn.
Zwei weitere Entscheidungen sind bemerkenswert: rider wurde vor der Berechnung gefüllt, hatte jedoch keinerlei Einfluss darauf — subset= hat ausschließlich fee und distance_km betrachtet. Und die Gebührensumme ist mit „(known)“ gekennzeichnet, wobei die Anzahl der fehlenden Gebühren direkt darunter ausgegeben wird, sodass niemand 540 fälschlicherweise für die gesamten Einnahmen hält.
Das Experiment. Ein reines dropna() entfernt auch 2004 und 2006 — bei denen Gebühr und Distanz völlig in Ordnung waren —, weil ihr Fahrer fehlte. Wenn du es vor dem Füllen von rider auf der Tabelle ausführst:
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.84Nur drei Lieferungen überleben, und das Verhältnis springt von 13.21 auf 16.84 — nicht etwa, weil sich eine Gebühr geändert hätte, sondern weil die beiden längsten Fahrten (12 und 6,5 km), die zufällig pro Kilometer günstiger sind, wegen eines fehlenden Namens herausgeworfen wurden. Die Spalte rider hat mit der Fragestellung nichts zu tun, sollte also auch nicht darüber entscheiden, welche Zeilen zählen. Genau dafür gibt es subset=.
Schritt 6 von 6
Herausforderung — das Kapitel-Quiz
Zehn Fragen von leicht bis schwer. Die letzten sind absichtlich knifflig.
Melde dich an, um das Quiz zu machen