Zeilen filtern — jeder Zeile dieselbe Frage stellen
Nur die Zeilen behalten, die mit Ja antworten: Boolesche Masken, Kombination mit &, | und ~, isin, between und den .str-Methoden, gefilterte Zeilen mit .loc — und genau diese Zeilen ändern, ganz ohne verkettete Zuweisung, die stillschweigend nichts bewirkt.
Wie möchtest du lernen?
Das Problem, das wir lösen
Die Filialleiterin schickt eine Nachricht: „Welche Bestellungen der Nordfiliale umfassten mehr als drei Artikel? Die möchte ich sehen.“
Bei den acht Bestellungen aus dem letzten Kapitel könntest du das noch mit einem Blick beantworten. Finger an die Spalte branch, bei jedem north anhalten, kurz zur Spalte quantity schauen. Drei Zeilen. Erledigt.
Jetzt stell dir die echte Datei vor — zwölftausend Bestellungen, ein ganzes Jahr. Das Auge reicht nicht mehr aus, und der erste Gedanke der meisten Python-Programmierenden ist eine Schleife:
import pandas as pd
orders = pd.read_csv("orders.csv")
keep = []
for i in range(len(orders)):
row = orders.iloc[i]
if row["branch"] == "north" and row["quantity"] > 3:
keep.append(int(row["order_id"]))
print(keep)[1001, 1005, 1008]Das Ergebnis stimmt. Aber schau, was es gekostet hat: einen Zähler, eine leere Liste, ein if, ein append, und die Tabelle wird Zeile für Zeile zerlegt. Das Ergebnis ist außerdem keine Tabelle mehr — es ist eine reine Liste von Bestellnummern. Um der Filialleiterin auch Produkte und Preise zu zeigen, müsstest du erneut eine Schleife schreiben. Bei zwölftausend Zeilen ist das zudem langsam, weil jedes orders.iloc[i] eine neue Series erzeugt, nur um daraus zwei Werte zu lesen.
Pandas bietet einen anderen Weg, und er ist das Häufigste, was du mit einem DataFrame je tun wirst: jeder Zeile gleichzeitig dieselbe Ja-oder-Nein-Frage stellen und dann die Zeilen behalten, die mit Ja geantwortet haben. Das ist Filtern, und darum geht es in diesem ganzen Kapitel. Die Schleife von oben wird zu einer Zeile, das Ergebnis bleibt eine Tabelle, und es liest sich fast wie der Satz der Filialleiterin.
Am Ende dieses Kapitels kannst du
- Eine Frage in eine boolesche Maske übersetzen — eine Spalte aus
True/False, eine pro Zeile — und sie nutzen, um Zeilen zu behalten oder sie mitmask.sum()undmask.mean()zu zählen - Bedingungen mit
&,|und~kombinieren und erklären, warum die Klammern zwingend erforderlich sind isin,betweenund die.str-Methoden für Listen, Bereiche und Textmuster verwenden- Zeilen filtern und Spalten in einem Schritt auswählen mit
.loc[mask, columns] - Werte in den gefilterten Zeilen sicher ändern mit
.loc[mask, column] = valueund denChainedAssignmentErrorerkennen, den die unsichere Methode in pandas 3 auslöst
Voraussetzungen: Spalten und Zeilen auswählen.
Erstelle zuerst die Datei
Dieses Kapitel verwendet dieselbe orders.csv wie das vorherige. Falls du sie noch nicht hast, lege sie in deinem Projektordner mit genau diesen neun Zeilen 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,5,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,east,bottle,accessories,7,120.0
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,850.0
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0Bevor du den Code schreibst
Ein Filter ist eine Frage, die in Code ausgedrückt wird. Die meisten Filterfehler sind keine pandas-Fehler — es ist eine Frage, die nie genau genug gestellt wurde, oder eine Spalte, die nicht das enthält, was man angenommen hat. Mache deshalb diese fünf Dinge, bevor du die erste Filterzeile schreibst.
1. Schreibe die Frage als Bedingungen auf, eine pro Spalte. Zerlege den Satz der Filialleiterin:
- „Bestellungen der Nordfiliale“ → Spalte
branch, Test gleich, Wert"north" - „mehr als drei Artikel“ → Spalte
quantity, Test größer als, Wert3 - „die möchte ich sehen“ → kein Test; die ganze Zeile behalten
Zwei Entscheidungen erzwingt diese Liste. „Mehr als drei“ ist > 3, nicht >= 3 — eine Bestellung von genau drei Artikeln zählt nicht. Und die beiden Bedingungen sind mit und verknüpft: eine Bestellung muss beide erfüllen. Diese zwei Entscheidungen bilden den gesamten Filter; der Code ist nur ihre Schreibweise.
2. Prüfe die Eingabe — Form, Typen und die genauen Werte, gegen die du vergleichst.
import pandas as pd
orders = pd.read_csv("orders.csv")
print(orders.shape)
print(orders.dtypes)(8, 7)
order_id int64
date str
branch str
product str
category str
quantity int64
price float64
dtype: objectquantity hat den Typ int64, also vergleicht > 3 Zahlen. Wäre es als str eingelesen worden — weil jemand "3 pcs" in eine Zelle getippt hat —, würde der Vergleich mit einem TypeError fehlschlagen, und das möchtest du jetzt wissen, nicht auf halbem Weg. branch ist str, der Vergleich ist also textbasiert und exakt: "north", "North" und "north " sind drei verschiedene Werte. Schau dir deshalb die tatsächlich vorhandenen Werte an:
print(orders["branch"].unique())<StringArray>
['north', 'south', 'east']
Length: 3, dtype: strDrei Filialen, jede einheitlich geschrieben, keine überflüssigen Leerzeichen. Jetzt ist garantiert, dass "north" im Code auch etwas trifft.
3. Skizziere das Ergebnis, bevor du es erzeugst. Bei einer kleinen Datei beantworte die Frage zuerst von Hand. Die Bestellungen der Nordfiliale sind 1001 (12 Artikel), 1003 (2), 1005 (30) und 1008 (4). Mehr als drei Artikel: 1001, 1005, 1008. Das erwartete Ergebnis sind also 3 Zeilen, alle 7 Spalten, und die Bestellnummern sind bekannt. Liefert der Code später 4 Zeilen oder 0, weißt du sofort, dass etwas falsch ist — statt blind dem Ergebnis zu vertrauen. Bei einer großen Datei geht das nicht vollständig von Hand, aber du kannst schätzen: „Die Nordfiliale hat etwa die Hälfte der Bestellungen, also sollte das Ergebnis deutlich weniger als die Hälfte der Zeilen sein.“
4. Wähle das Werkzeug anhand der Form der Bedingung.
- ein Vergleich → eine Maske:
df["col"] > value - mehrere Bedingungen → Masken verknüpft mit
&(und),|(oder),~(nicht) - „einer dieser Werte“ →
df["col"].isin([...]) - „zwischen zwei Werten“ →
df["col"].between(low, high) - ein Muster im Text →
df["col"].str.contains(...),.str.startswith(...) - Zeilen und nur bestimmte Spalten →
df.loc[mask, ["col1", "col2"]] - Werte in diesen Zeilen ändern →
df.loc[mask, "col"] = new_value
Die Frage der Filialleiterin besteht aus zwei Vergleichen, die mit und verknüpft sind: zwei Masken und &.
5. Entscheide, wie du das Ergebnis prüfst. Nach dem Filtern decken drei einfache Prüfungen fast alles auf: Die Zeilenanzahl stimmt mit deiner Skizze überein; die Bedingung gilt wirklich in jeder übrig gebliebenen Zeile ((result["quantity"] > 3).all() sollte True ausgeben); und nichts Erwartetes fehlt (suche in der Ergebnismenge nach 1008).
Jetzt der Code — ein Konzept nach dem anderen.
Ein Vergleich ergibt eine Maske
Vergleiche eine Spalte mit einem Wert, und pandas gibt dir nicht ein einzelnes True oder False zurück. Es vergleicht jede Zeile und liefert eine ganze Spalte mit Antworten:
big = orders["quantity"] > 5
print(big)0 True
1 False
2 False
3 True
4 True
5 False
6 True
7 False
Name: quantity, dtype: boolDas ist eine Series aus bool — eine boolesche Maske. Sie hat denselben Index wie orders (0 bis 7), eine Antwort pro Zeile: Zeile 0 hat 12 Artikel, also True; Zeile 1 hat 5, und 5 ist nicht größer als 5, also False.
Warum arbeitet pandas so, statt dich eine Schleife schreiben zu lassen? Weil eine Spalte als ein zusammenhängender Block von Zahlen eines einzigen Typs gespeichert ist, und den gesamten Block mit 5 zu vergleichen, ist eine Operation, die in schnellem, kompiliertem Code ausgeführt wird. Deine Schleife hat Python angewiesen, jede Zeile einzeln zu betrachten, achtmal — oder zwölftausendmal. Die Maske fragt einmal.
Jeder Vergleichsoperator funktioniert gleich: ==, !=, <, <=, >, >=. Sie funktionieren auch auf Textspalten — orders["branch"] == "north" ist eine Maske mit True in den Zeilen 0, 2, 4 und 7.
Noch wurde nichts gefiltert. Eine Maske liefert nur die Antworten. Im nächsten Schritt werden sie verwendet.
Die Maske behält Zeilen
Lege die Maske in eckige Klammern, und pandas behält die Zeilen, bei denen die Maske True ist:
print(orders[big])order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
3 1004 2024-03-02 east bottle accessories 7 120.0
4 1005 2024-03-03 north eraser stationery 30 8.0
6 1007 2024-03-04 east pen stationery 20 15.0Vier von acht Zeilen. Lies den Index links: 0, 3, 4, 6. Die verbliebenen Zeilen behalten ihre ursprünglichen Labels. Beim Filtern wird nichts neu nummeriert — Zeile 3 ist weiterhin Zeile 3, dieselbe Bestellung 1004 wie in der vollständigen Tabelle. Das spielt eine Rolle, und wir kommen darauf zurück.
Wie ordnet pandas die Maske den Zeilen zu? Nicht nach Position — nach Index-Label. Label 0 der Maske entscheidet über Zeile 0, Label 3 über Zeile 3. Hier sind die beiden Indizes identisch, weil die Maske aus orders selbst aufgebaut wurde, sodass das unsichtbar bleibt. Es wird nur sichtbar, wenn du eine Maske verwendest, die aus einer anderen Tabelle stammt — dazu mehr unter „Wenn etwas schiefgeht“.
Die Maske wird meistens direkt inline geschrieben, ohne einen Namen:
print(orders[orders["branch"] == "north"])order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
2 1003 2024-03-02 north bag accessories 2 850.0
4 1005 2024-03-03 north eraser stationery 30 8.0
7 1008 2024-03-04 north bottle accessories 4 120.0Lies es von innen nach außen: `orders["branch"] == "north"` baut die Maske, `orders[ … ]` behält die True-Zeilen. Die Maske zu benennen (big = …) lohnt sich, wenn die Bedingung lang ist oder wenn du sie zweimal verwendest; inline ist für kurze Bedingungen gut.
Mit einer Maske zählen, ohne zu filtern
Oft will die Filialleiterin keine Zeilen, sondern nur eine Zahl: Wie viele Bestellungen umfassten mehr als fünf Artikel? Dafür muss man nicht filtern. In der Arithmetik zählt True als 1 und False als 0, also:
print(big.sum())
print(big.mean())4
0.5sum() addiert die Einsen: 4 Bestellungen. mean() dividiert durch die Anzahl der Zeilen: 4 von 8 ist 0.5, also die Hälfte der Bestellungen umfasste mehr als fünf Artikel. „Wie viele“ ist .sum() einer Maske; „welcher Anteil“ ist .mean() einer Maske. Beides ist eine Zeile, und keine davon baut eine neue Tabelle.
Und wenn du eine Summe über die gefilterten Zeilen willst — die Gesamtzahl der Artikel in den großen Bestellungen —, filtere zuerst, dann wähle die Spalte, dann summiere:
print(orders[big]["quantity"].sum())6912 + 7 + 30 + 20 = 69. Eine Spalte einer gefilterten Tabelle so zu lesen ist in Ordnung. Schreiben in eine Tabelle auf diese Weise ist es nicht — darum geht es im letzten Abschnitt des Kapitels.
Mehrere Bedingungen: &, |, ~
Die Frage der Filialleiterin hat zwei Bedingungen. Pandas verknüpft Masken mit drei Operatoren:
&bedeutet und — die Zeile wird behalten, wenn beide MaskenTruesind|bedeutet oder — die Zeile wird behalten, wenn mindestens eine MaskeTrueist~bedeutet nicht — es kehrt jedesTruezuFalseum und umgekehrt
Hier ist die Frage der Filialleiterin, endlich:
north_big = orders[(orders["branch"] == "north") & (orders["quantity"] > 3)]
print(north_big)order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
4 1005 2024-03-03 north eraser stationery 30 8.0
7 1008 2024-03-04 north bottle accessories 4 120.0Drei Zeilen: 1001, 1005, 1008 — genau die Skizze aus dem Planungsschritt. Jetzt die Prüfung aus Schritt 5:
print(len(north_big))
print((north_big["quantity"] > 3).all())
print((north_big["branch"] == "north").all())3
True
TrueDie Anzahl stimmt, und beide Bedingungen gelten in jeder verbliebenen Zeile. .all() fragt „Ist jeder Wert in dieser Maske True?“ — das ist die günstigste Möglichkeit zu beweisen, dass ein Filter das gemacht hat, was man beabsichtigte.
Eine Oder-Frage: Bestellungen der Ostfiliale oder jede Bestellung eines Artikels, der 850 oder mehr kostet.
print(orders[(orders["branch"] == "east") | (orders["price"] >= 850)])order_id date branch product category quantity price
2 1003 2024-03-02 north bag accessories 2 850.0
3 1004 2024-03-02 east bottle accessories 7 120.0
5 1006 2024-03-03 south bag accessories 1 850.0
6 1007 2024-03-04 east pen stationery 20 15.0Vier Zeilen: die zwei Bestellungen der Ostfiliale und die zwei Taschenbestellungen aus Nord und Süd. Eine Zeile, die beide Bedingungen erfüllt (hier keine), würde nur einmal erscheinen — ein Filter behält oder entfernt eine Zeile immer nur einmal.
Eine Nicht-Frage: alles außer Schreibwaren.
print(orders[~(orders["category"] == "stationery")])order_id date branch product category quantity price
2 1003 2024-03-02 north bag accessories 2 850.0
3 1004 2024-03-02 east bottle accessories 7 120.0
5 1006 2024-03-03 south bag accessories 1 850.0
7 1008 2024-03-04 north bottle accessories 4 120.0~ kehrt jedes True zu False um und umgekehrt. Bei einem einfachen Gleichheitstest könnte man auch != schreiben, was einfacher ist; ~ bewährt sich, wenn das Negierte eine längere Bedingung oder ein isin ist (das siehst du bald).
Warum die Klammern zwingend sind
Jede Bedingung oben steht in eigenen Klammern. Das ist keine bloße Stilfrage. Lasse sie weg, und der Code bricht:
print(orders[orders["quantity"] > 3 & orders["price"] < 100])TypeError: Cannot perform 'rand_' with a dtyped [float64] array and scalar of type [bool]Was du oben siehst, ist die letzte Zeile des Tracebacks, die den Fehler benennt; die Zeilen darüber sagen nur, wo er aufgetreten ist. Der Grund ist die Operatorrangfolge — die Reihenfolge, in der Python Operatoren anwendet. In Python bindet & stärker als > und <. Das wurde lange vor pandas so festgelegt, weil & für ganze Zahlen (bitweises „und“) gedacht war, wo es zuerst angewendet werden soll. Python liest die Zeile also so:
orders["quantity"] > (3 & orders["price"]) < 100Es versucht zuerst 3 & orders["price"] zu berechnen — ein bitweises „und“ zwischen der Zahl 3 und einer Spalte aus Dezimalzahlen — und genau darum geht es in der Fehlermeldung. Die Meldung erwähnt rand_ (das „rechte und“) und ein Array, aber die Ursache ist immer dieselbe: ein fehlendes Klammerpaar.
Der Fehler ist noch das glückliche Ergebnis. Umklammere nur die erste Bedingung und vergiss die zweite, und es gibt gar keinen Fehler:
print(len(orders[(orders["quantity"] > 3) & orders["price"] < 100]))
print(len(orders[(orders["quantity"] > 3) & (orders["price"] < 100)]))8
4Die erste Zeile wird gelesen als ((quantity > 3) & price) < 100. Das & vermischt eine Maske mit den Preisen, und jedes Ergebnis liegt zufällig unter 100, sodass der „Filter“ alle acht Zeilen behält. Der korrekte Filter behält vier. Dieselben Spalten, dieselben Zahlen, ein fehlendes Klammerpaar — und die falsche Version läuft ohne einen Ton. Setze jede Bedingung in eigene Klammern, jedes Mal. Dann kann weder der Fehler noch die stille Variante auftreten.
Warum nicht and?
Der andere natürliche Impuls ist, das englische Schlüsselwort zu schreiben:
print(orders[(orders["branch"] == "north") and (orders["quantity"] > 3)])ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().Pythons and, or und not brauchen auf jeder Seite ein einzelnes True oder False — sie wurden für if-Ausdrücke gebaut. Um and zu verwenden, fragt Python die erste Maske „Bist du wahr?“, und eine Spalte mit acht Antworten kann nicht mit einer einzigen antworten. Gilt sie als wahr, wenn irgendein Wert True ist? Wenn alle es sind? Pandas weigert sich zu raten, und die Fehlermeldung listet die Möglichkeiten auf, wie du entscheiden könntest (a.any(), a.all()). Aber du wolltest keine einzelne Antwort; du wolltest ein zeilenweises „und“. Das ist &. Die Regel zum Merken: In einem Filter werden and/or/not zu &/|/~.
Einer von mehreren Werten: isin
Bestellungen der Süd- oder Ostfiliale. Du könntest zwei Gleichheitstests mit | verknüpfen. Bei fünf Filialen werden das fünf Vergleiche. isin nimmt eine Liste und fragt „Ist der Wert dieser Zeile in der Liste?“:
print(orders[orders["branch"].isin(["east", "south"])])order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery 5 60.0
3 1004 2024-03-02 east bottle accessories 7 120.0
5 1006 2024-03-03 south bag accessories 1 850.0
6 1007 2024-03-04 east pen stationery 20 15.0Und hier zeigt ~ seinen Wert — jede Filiale außer Nord und Ost ist ein ~ vor einem isin:
print(orders[~orders["branch"].isin(["north", "east"])]["order_id"].tolist())[1002, 1006]Zwei Bestellungen, beide aus der Südfiliale. Beachte, dass ~orders["branch"].isin([...]) keine zusätzlichen Klammern braucht: ~ wird auf das Ergebnis des Methodenaufrufs angewendet, und es gibt kein & oder |, das damit konkurriert. Sobald du es mit einer anderen Bedingung kombinierst, kommen die Klammern zurück: (~orders["branch"].isin([...])) & (orders["quantity"] > 3).
Ein Bereich: between
Bestellungen mit einem Preis von 15 bis 120. Das sind zwei Vergleiche, >= 15 und <= 120. between sagt es in einem:
print(orders[orders["price"].between(15, 120)])order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
1 1002 2024-03-01 south notebook stationery 5 60.0
3 1004 2024-03-02 east bottle accessories 7 120.0
6 1007 2024-03-04 east pen stationery 20 15.0
7 1008 2024-03-04 north bottle accessories 4 120.0between schließt beide Grenzen standardmäßig ein — die Stifte bei genau 15,0 und die Flaschen bei genau 120,0 sind dabei. Das entspricht der üblichen Sprechweise „von 15 bis 120“, aber prüfe es gegen die Frage. Wenn die Frage strikt zwischen den Grenzen meint, gib das an:
print(orders["price"].between(15, 120, inclusive="neither").sum())1Nur das Notizbuch bei 60,0 liegt strikt dazwischen. inclusive akzeptiert auch "left" und "right" für halboffene Bereiche — die übliche Wahl für Preisbänder, wo man 0–100, 100–200 und so weiter ohne einen Grenzwert in zwei Bändern haben möchte.
Textmuster: die .str-Methoden
Gleichheit bei Text ist exakt. Der .str-Accessor gibt einer Textspalte die String-Methoden, die du aus Python kennst, angewendet auf jede Zeile, und jede gibt eine Maske zurück.
Produkte, deren Name mit „b“ beginnt:
print(orders[orders["product"].str.startswith("b")])order_id date branch product category quantity price
2 1003 2024-03-02 north bag accessories 2 850.0
3 1004 2024-03-02 east bottle accessories 7 120.0
5 1006 2024-03-03 south bag accessories 1 850.0
7 1008 2024-03-04 north bottle accessories 4 120.0Produkte, die den Buchstaben „o“ enthalten, unabhängig von der Groß-/Kleinschreibung:
print(orders[orders["product"].str.contains("O", case=False)])order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery 5 60.0
3 1004 2024-03-02 east bottle accessories 7 120.0
7 1008 2024-03-04 north bottle accessories 4 120.0Es gibt kein großes O in den Daten, aber das Notizbuch und die zwei Flaschen werden gefunden, weil case=False die Groß-/Kleinschreibung ignoriert. Ohne diese Option würde .str.contains("O") nichts finden.
Exakte Gleichheit kennt diese Option nicht. Das ist der häufigste „Mein Filter gibt nichts zurück“-Fehler:
print(orders[orders["branch"] == "North"])Empty DataFrame
Columns: [order_id, date, branch, product, category, quantity, price]
Index: []Kein Fehler, keine Zeilen. Die Tabelle ist nicht falsch und pandas ist nicht falsch — es gibt einfach keine Filiale, die mit großem Anfangsbuchstaben "North" geschrieben ist. Deshalb hat Schritt 2 des Plans unique() ausgegeben: Die Werte, gegen die du vergleichst, sollten aus dem kopiert werden, was tatsächlich in der Spalte steht, nicht aus dem Gedächtnis getippt werden.
Datumsangaben als Text lassen sich korrekt vergleichen — wenn sie im ISO-Format vorliegen
Die Spalte date kam als str an. Kann man sie mit >= filtern? Bestellungen ab dem 3. März:
print(orders["date"] >= "2024-03-03")0 False
1 False
2 False
3 False
4 True
5 True
6 True
7 True
Name: date, dtype: boolEs funktioniert, aber verstehe warum. Das sind Strings, also vergleicht >= sie als Text, Zeichen für Zeichen, so wie ein Wörterbuch sortiert. Das ergibt das richtige Ergebnis nur, weil die Daten im Format Jahr-Monat-Tag mit führenden Nullen geschrieben sind (2024-03-03), sodass die Textreihenfolge zufällig der Zeitreihenfolge entspricht. Bei einer Spalte im Format 3/3/2024 würde der Vergleich falsch ausfallen — "10/1/2024" sortiert vor "9/1/2024", weil "1" vor "9" kommt. Für alles außer einfachen Grenzwerten wandle die Werte mit pd.to_datetime in echte Datumswerte um — das wird im Kapitel über das Hinzufügen von Spalten behandelt.
Zeilen und Spalten zusammen: .loc[mask, columns]
Bisher haben die Filter alle Spalten behalten. Die Filialleiterin wollte die Bestellungen der Nordfiliale sehen, aber der Bericht braucht nur Produkt und Menge. Du hast .loc[rows, columns] im letzten Kapitel mit Labels kennengelernt. Der Zeilen-Teil akzeptiert auch eine Maske:
print(orders.loc[orders["branch"] == "north", ["product", "quantity"]])product quantity
0 pen 12
2 bag 2
4 eraser 30
7 bottle 4Ein Schritt, ein Objekt, und wer deinen Code liest, sieht die gesamte Frage — welche Zeilen, welche Spalten — an einer Stelle. .loc[mask, "quantity"] (ein einzelner Spaltenname, keine Liste) liefert dir eine Series, bereit für Berechnungen:
print(orders.loc[orders["branch"] == "north", "quantity"].sum())4812 + 2 + 30 + 4 = 48 Artikel, die von der Nordfiliale verkauft wurden.
query(): derselbe Filter als Satz
Es gibt eine zweite Schreibweise für Filter: query(), die die Bedingung als String entgegennimmt:
print(orders.query("branch == 'north' and quantity > 3"))order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
4 1005 2024-03-03 north eraser stationery 30 8.0
7 1008 2024-03-04 north bottle accessories 4 120.0Dieselben drei Zeilen. Innerhalb des Strings schreibst du einfache Spaltennamen, und and/or/not sind erlaubt, weil query den String selbst parst statt ihn Pythons Operatoren zu übergeben. Um eine Python-Variable zu verwenden, stelle ihr @ voran:
min_qty = 5
print(orders.query("quantity >= @min_qty")["order_id"].tolist())[1001, 1002, 1004, 1005, 1007]query liest sich gut bei langen Bedingungen. Die Nachteile: Ein Tippfehler im String wird erst entdeckt, wenn die Zeile ausgeführt wird, dein Editor kann dir dabei nicht helfen, und Spaltennamen mit Leerzeichen brauchen Backticks. Dieser Kurs verwendet Masken als Hauptwerkzeug, weil Masken die Grundlage für alles andere in pandas sind — .loc, Zuweisung, Zählen. Kenne query, damit du es im Code anderer lesen kannst.
Die gefilterte Tabelle behält ihre alten Labels
Zurück zum Index. Speichere die Bestellungen der Nordfiliale und sieh sie dir an:
north = orders[orders["branch"] == "north"]
print(north.index.tolist())[0, 2, 4, 7]Labels 0, 2, 4, 7. Was ist also north.loc[1]? Es gibt kein Label 1 in dieser Tabelle — Bestellung 1002 kam aus der Südfiliale und wurde herausgefiltert:
print(north.loc[1])KeyError: 1.loc sucht nach einem Label, und Label 1 existiert hier nicht. Wenn du „die zweite Bestellung der Nordfiliale“ meinst, ist das eine Position, und Positionen sind .iloc:
print(north.iloc[1])order_id 1003
date 2024-03-02
branch north
product bag
category accessories
quantity 2
price 850.0
Name: 2, dtype: objectDie zweite Zeile nach Position ist die Taschenbestellung — und ihr Name ist 2, ihr Label aus der ursprünglichen Tabelle. Das ist genau der loc/iloc-Unterschied aus dem letzten Kapitel, und beim Filtern wird er spürbar, weil Labels und Positionen nach einem Filter nicht mehr übereinstimmen.
Die Labels zu behalten ist bewusst so: Es erlaubt dir, jede Zeile eines Ergebnisses zur ursprünglichen Tabelle zurückzuverfolgen. Wenn du das nicht mehr brauchst — etwa weil die gefilterte Tabelle der finale Bericht ist —, nummeriere sie neu:
print(north.reset_index(drop=True))order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
1 1003 2024-03-02 north bag accessories 2 850.0
2 1005 2024-03-03 north eraser stationery 30 8.0
3 1008 2024-03-04 north bottle accessories 4 120.0drop=True wirft die alten Labels weg. Ohne das würden sie als neue Spalte namens index behalten werden, was in einem Bericht selten gewünscht ist.
Ein leeres Ergebnis ist kein Fehler
none = orders[orders["branch"] == "west"]
print(none)
print(none.shape)
print(none.empty)Empty DataFrame
Columns: [order_id, date, branch, product, category, quantity, price]
Index: []
(0, 7)
TrueEs gibt keine Westfiliale, also keine Zeilen — und keinen Fehler. Das ist korrektes Verhalten (Null ist eine gültige Antwort), bedeutet aber, dass ein falsch geschriebener Filter still versagt. Summen über ein leeres Ergebnis sind 0 und Mittelwerte sind nan, was sich weit durch einen Bericht ziehen kann, bevor es jemand bemerkt. Wenn ein Filter die Daten für etwas Wichtiges liefert, prüfe result.empty oder die Zeilenanzahl und teile das ausdrücklich mit: „Keine Bestellungen aus der Westfiliale.“ ist besser als eine leere Tabelle.
Die gefilterten Zeilen ändern: .loc[mask, column] = value
Manchmal filterst du, um etwas zu ändern. Die Stifte der Ostfiliale waren falsch bepreist; sie hätten 13,5 statt 15,0 kosten sollen. Der naheliegende Code macht den Filter und wählt dann die Spalte — zwei eckige Klammerpaare, so wie du Werte vorhin gelesen hast:
import pandas as pd
orders = pd.read_csv("orders.csv")
orders[orders["branch"] == "east"]["price"] = 13.5
print(orders["price"].tolist())[15.0, 60.0, 850.0, 120.0, 8.0, 850.0, 15.0, 120.0]
ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment.
Such chained assignment 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).
Try using '.loc[row_indexer, col_indexer] = value' instead, to perform the assignment in a single step.
See the documentation for a more detailed explanation: https://pandas.pydata.org/pandas-docs/stable/user_guide/copy_on_write.html#chained-assignmentLies die Preise: Nichts hat sich geändert. Und pandas hat dich ausführlich gewarnt. (Im Terminal erscheint die Warnung in stderr, also über der Liste, in dem Moment, in dem die Zuweisung ausgeführt wird, und beginnt mit dem Dateinamen und der Zeilennummer. Hier wird sie nach der Ausgabe gezeigt, ohne diese Angaben.) Das ist verkettete Zuweisung — zwei Indexierungsschritte, orders[mask] und dann ["price"], mit der Zuweisung am Ende der Kette. Der erste Schritt, orders[mask], baut eine neue Tabelle mit den Zeilen der Ostfiliale. Der zweite Schritt setzt Preise in dieser neuen Tabelle. Dann wird die neue Tabelle weggeworfen, weil nichts sie festhält. orders selbst wurde nie berührt.
In pandas 3 ist das garantiertes Verhalten, genannt Copy-on-Write: Jede Tabelle, die du durch Indexierung erhältst, verhält sich als eigenständige Kopie, sodass das Schreiben in sie niemals in die ursprüngliche Tabelle zurückreichen kann. Der ChainedAssignmentError ist eine Warnung, keine Exception (Ausnahme) — das Programm läuft weiter —, was genau der Grund ist, warum er gefährlich ist: Ein Skript mit dieser Zeile beendet sich „erfolgreich“ mit den alten Preisen noch an Ort und Stelle.
(Wenn du ältere Tutorials liest, wirst du diese Situation mit einer SettingWithCopyWarning und dem Hinweis beschrieben finden, dass es „möglicherweise funktioniert oder nicht“. In pandas 3 ist die Antwort einfach: Es funktioniert nie, also schreibe es nie so.)
Die Lösung ist der eigene Rat der Warnung: ein Indexierungsschritt, .loc[rows, column], mit der Zuweisung darauf:
orders.loc[orders["branch"] == "east", "price"] = 13.5
print(orders["price"].tolist())[15.0, 60.0, 850.0, 13.5, 8.0, 850.0, 13.5, 120.0]Warte — das hat beide Bestellungen der Ostfiliale geändert, den Stift und die Flasche. Die Flasche war nicht falsch bepreist. Der Code hat genau das gemacht, was er gesagt hat; die Frage im Code war falsch. Das ist Schritt 1 des Plans, der zurückkommt: „Stifte der Ostfiliale“ sind zwei Bedingungen, und nur eine wurde geschrieben. Fange mit einer frischen Kopie der Datei neu an und schreibe beide:
import pandas as pd
orders = pd.read_csv("orders.csv")
fix = (orders["branch"] == "east") & (orders["product"] == "pen")
print(fix.sum())
orders.loc[fix, "price"] = 13.5
print(orders.loc[orders["branch"] == "east", ["product", "price"]])1
product price
3 bottle 120.0
6 pen 13.5Zwei Gewohnheiten sind hier sichtbar. Die Maske bekommt einen Namen (fix), weil sie für etwas Wichtiges verwendet wird; und vor dem Schreiben sagt fix.sum(), wie viele Zeilen gleich geändert werden — eine, was erwartet wurde. Zähle, bevor du überschreibst, jedes Mal.
Zum Lesen istdf[mask]["col"]in Ordnung. Zum Schreiben verwende immerdf.loc[mask, "col"] = value. Die verkettete Form löst keine Exception aus, und die Änderung verschwindet still.
Eine gefilterte Tabelle zu speichern, um damit zu arbeiten, ist in Ordnung
Was ist mit diesem Fall?
import pandas as pd
orders = pd.read_csv("orders.csv")
east = orders[orders["branch"] == "east"]
east["price"] = 0.0
print(east["price"].tolist())
print(orders["price"].tolist())[0.0, 0.0]
[15.0, 60.0, 850.0, 120.0, 8.0, 850.0, 15.0, 120.0]Keine Warnung, und das ist richtig so. Hier wolltest du absichtlich eine separate Tabelle: Du hast sie east genannt, und sie zu ändern ändert nur east. orders bleibt unberührt. Copy-on-Write macht dieses Verhalten vorhersehbar — eine Tabelle, die du durch Filtern erhalten hast, ist immer unabhängig von der ursprünglichen. Der Fehler ist nur die verkettete Form, bei der die Zwischentabelle keinen Namen hat und die Änderung mit ihr verschwindet.
Ein vollständiges Beispiel
Die Filialleiterin möchte jetzt einen kurzen Bericht. report.py:
import pandas as pd
orders = pd.read_csv("orders.csv")
# Check what arrived before asking it anything.
print("Rows, columns:", orders.shape)
print("Branches:", list(orders["branch"].unique()))
print()
# 1. The original question: north-branch orders of more than three items.
north_big = orders.loc[
(orders["branch"] == "north") & (orders["quantity"] > 3),
["order_id", "product", "quantity"],
]
print("North orders over 3 items:")
print(north_big.reset_index(drop=True))
print()
# 2. Counting with masks: no new table needed.
accessories = orders["category"] == "accessories"
print("Accessory orders :", accessories.sum())
print("Share of all orders:", accessories.mean())
print("Accessory items :", orders.loc[accessories, "quantity"].sum())
print()
# 3. Outside north, cheap items (under 100).
cheap_outside = orders[
(orders["branch"] != "north") & (orders["price"] < 100)
]
print("Cheap orders outside north:", cheap_outside["order_id"].tolist())
# 4. A branch we do not have: say so instead of printing an empty table.
west = orders[orders["branch"] == "west"]
if west.empty:
print("No orders from the west branch.")Rows, columns: (8, 7)
Branches: ['north', 'south', 'east']
North orders over 3 items:
order_id product quantity
0 1001 pen 12
1 1005 eraser 30
2 1008 bottle 4
Accessory orders : 4
Share of all orders: 0.5
Accessory items : 14
Cheap orders outside north: [1002, 1007]
No orders from the west branch.Warum es so geschrieben ist:
- Zuerst prüfen, dann fragen.
shapeund die Liste der Filialen werden ausgegeben, bevor ein Filter ausgeführt wird. Wäre die Datei mit großgeschriebenemNorthangekommen, würde die Filialen-Zeile das zeigen, bevor der Bericht eine leere Tabelle ausgibt. .loc[mask, columns]für die Hauptfrage. Die Zeilen und Spalten der Antwort werden an einer Stelle entschieden, undreset_index(drop=True)nummeriert das Ergebnis neu, weil dies der finale Bericht ist, kein Zwischenschritt.- Eine benannte Maske, dreimal verwendet.
accessorieswird einmal berechnet und dann gezählt (sum), gemessen (mean) und verwendet, um eine Spalte auszuwählen. Eine Maske zu benennen verhindert, dass du dieselbe Bedingung dreimal schreiben musst und eine davon versehentlich leicht abweicht. - Jede Bedingung in eigenen Klammern, sogar bei den kurzen, damit das Hinzufügen einer dritten Bedingung später nicht den Rangfolge-Fehler auslösen kann.
- Der leere Fall wird ausdrücklich behandelt. Null Zeilen ist eine gültige Antwort, und der Bericht sagt es in Worten.
Wenn etwas schiefgeht
ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all(). Du hast and, or oder not zwischen zwei Masken verwendet — oder ein if auf eine ganze Maske angewendet. Pythons Schlüsselwörter brauchen ein einzelnes True/False, und eine Maske hat eines pro Zeile. In einem Filter schreibe &, |, ~. Entscheide in einem if, was du meinst: if mask.any(): („Ist mindestens eine Zeile wahr?“) oder if mask.all(): („Sind alle Zeilen wahr?“).
TypeError: Cannot perform 'rand_' with a dtyped [float64] array and scalar of type [bool] (oder 'ror_', oder [int64]) Fehlende Klammern um eine Bedingung neben & oder |. Python hat & vor > angewendet, also versuchte es ein bitweises „und“ zwischen einer Zahl und einer Spalte. Setze jede Bedingung in eigene Klammern: (a > 3) & (b < 100).
Der Filter gibt eine leere Tabelle zurück, ohne Fehler Dein Wert kommt in der Spalte nicht genau so vor, wie du ihn getippt hast. Gib df["col"].unique() aus — oder besser list(df["col"].unique()), was jeden Wert in Anführungszeichen zeigt, sodass ein abschließendes Leerzeichen in 'north ' sichtbar wird. Bereinige die Daten mit df["col"].str.strip() oder passe die Suche mit .str.contains(..., case=False) an.
KeyError: 1 (oder eine andere Zahl) direkt nach dem Filtern Du hast .loc[n] auf einer gefilterten Tabelle verwendet, als hätte sie noch die Labels 0, 1, 2…. Sie behält die ursprünglichen Labels. Verwende .iloc[n] für „die n-te Zeile“ oder reset_index(drop=True), wenn die alten Labels nicht mehr gebraucht werden.
ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment. Du hast df[mask]["col"] = value geschrieben. Das ändert df in pandas 3 nie — die Warnung teilt dir mit, dass die Zeile nichts bewirkt hat. Schreibe df.loc[mask, "col"] = value.
UserWarning: Boolean Series key will be reindexed to match DataFrame index. Die Maske wurde aus einer anderen Tabelle aufgebaut als die, die du filterst — typischerweise eine Maske aus dem vollständigen orders, die auf dem gefilterten north verwendet wird. Pandas richtet sie nach Label aus und lässt Labels, die es nicht findet, still wegfallen:
import pandas as pd
orders = pd.read_csv("orders.csv")
north = orders[orders["branch"] == "north"]
big = orders["quantity"] > 5
print(north[big])order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
4 1005 2024-03-03 north eraser stationery 30 8.0
UserWarning: Boolean Series key will be reindexed to match DataFrame index.(Im Terminal erscheint die Warnung über der Tabelle.) Es gibt hier zufällig eine sinnvolle Antwort, aber nur durch Glück der Labels. Baue die Maske aus der Tabelle, die du filterst: north[north["quantity"] > 5].
Schritt 4 von 6 — Vorhersagen
Prüfe dein Verständnis
Was wird ausgegeben?
from io import StringIO
import pandas as pd
# Stands in for orders.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,5,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,east,bottle,accessories,7,120.0
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,850.0
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))
mask = orders["quantity"] > 5
print(mask.sum(), len(orders[mask]))- A5 5
- B4 8
- C4 4
- DTrue 4
Dieser Code soll Bestellungen der Nordfiliale mit mehr als drei Artikeln behalten. Was passiert bei der Ausführung?
from io import StringIO
import pandas as pd
# Stands in for orders.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,5,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,east,bottle,accessories,7,120.0
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,850.0
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))
north_big = orders[orders["branch"] == "north" & orders["quantity"] > 3]- AEin `ValueError: The truth value of a Series is ambiguous`
- BEs gibt eine leere Tabelle zurück
- CEs funktioniert und behält die Bestellungen 1001, 1005 und 1008
- DEin `TypeError`: Ohne Klammern wird `&` vor `==` und `>` angewendet
Du musst den Preis jeder Tasche in orders selbst halbieren. Welche Zeile tut das?
- Aorders[orders["product"] == "bag"]["price"] = orders["price"] / 2
- Bbags = orders[orders["product"] == "bag"] bags["price"] = bags["price"] / 2
- Corders["price"][orders["product"] == "bag"] /= 2
- Dorders.loc[orders["product"] == "bag", "price"] = orders["price"] / 2
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
Verwende dieselbe orders.csv. Schreibe filters.py, das vier Fragen beantwortet, jede unter einer kurzen Überschrift:
- Wie viele Bestellungen umfassten 10 oder mehr Artikel? Gib eine Zahl aus, keine Tabelle.
- Zubehörbestellungen außerhalb der Nordfiliale — nur
order_id,branch,productundprice, neu nummeriert ab 0. - Bestellungen aus der Süd- oder Ostfiliale, deren Preis unter 100 liegt. Verwende
isinfür die Filialen. - Die Zentrale gewährt 10% Rabatt auf jede Schreibwarenbestellung mit 20 oder mehr Artikeln. Setze den
pricegenau dieser Zeilen auf 90% des aktuellen Preises, aber gib zuerst aus, wie viele Zeilen du gleich ändern wirst. Dann giborder_id,quantityundpricefür alle Schreibwarenbestellungen aus.
Plane jede Frage, bevor du Code schreibst: welche Spalten, welche Tests, womit verknüpft und welche Zeilen du durch einen Blick in die Datei erwartest. Wenn dein Programm korrekt ist, gibt es genau das aus:
1. Orders of 10+ items:
3
2. Accessory orders outside north:
order_id branch product price
0 1004 east bottle 120.0
1 1006 south bag 850.0
3. South or east, price under 100:
order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery 5 60.0
6 1007 2024-03-04 east pen stationery 20 15.0
4. Stationery discount:
Rows to change: 2
order_id quantity price
0 1001 12 15.0
1 1002 5 60.0
4 1005 30 7.2
6 1007 20 13.5Dann brich es absichtlich, einmal für jeden Fall:
- Entferne die Klammern in Frage 3. Welcher Fehler erscheint?
- Schreibe Frage 4 als
orders[mask]["price"] = .... Was gibt pandas aus, und hat sich ein Preis geändert? - Ändere
"east"zu"East"in Frage 3. Was passiert, und warum ist das schlimmer als ein Fehler?
Lösung
Zuerst planen. Jede Frage, aufgeschlüsselt:
quantity >= 10, eine Bedingung. Auf einen Blick: 12, 30 und 20, also 3.category == "accessories"undbranch != "north". Auf einen Blick: 1004 (Ost, Flasche) und 1006 (Süd, Tasche).branchin Süd/Ost undprice < 100. Auf einen Blick: 1002 (Notizbuch, 60,0) und 1007 (Stift, 15,0).category == "stationery"undquantity >= 20. Auf einen Blick: 1005 (Radiergummi, 30) und 1007 (Stift, 20), also 2 Zeilen.
Frage 4 erfordert Sorgfalt: „20 Artikel oder mehr“ ist >= 20, also ist Bestellung 1007 mit genau 20 Artikeln eingeschlossen. Und sie schreibt, also verwendet sie .loc[mask, "price"] mit einer benannten Maske, die zuerst gezählt wird.
filters.py:
import pandas as pd
orders = pd.read_csv("orders.csv")
print("1. Orders of 10+ items:")
print((orders["quantity"] >= 10).sum())
print()
print("2. Accessory orders outside north:")
outside = orders.loc[
(orders["category"] == "accessories") & (orders["branch"] != "north"),
["order_id", "branch", "product", "price"],
]
print(outside.reset_index(drop=True))
print()
print("3. South or east, price under 100:")
cheap = orders[
(orders["branch"].isin(["south", "east"])) & (orders["price"] < 100)
]
print(cheap)
print()
print("4. Stationery discount:")
discount = (orders["category"] == "stationery") & (orders["quantity"] >= 20)
print("Rows to change:", discount.sum())
orders.loc[discount, "price"] = orders.loc[discount, "price"] * 0.9
print(orders.loc[orders["category"] == "stationery", ["order_id", "quantity", "price"]])1. Orders of 10+ items:
3
2. Accessory orders outside north:
order_id branch product price
0 1004 east bottle 120.0
1 1006 south bag 850.0
3. South or east, price under 100:
order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery 5 60.0
6 1007 2024-03-04 east pen stationery 20 15.0
4. Stationery discount:
Rows to change: 2
order_id quantity price
0 1001 12 15.0
1 1002 5 60.0
4 1005 30 7.2
6 1007 20 13.5Jede Antwort stimmt mit dem Plan überein. Anmerkungen zu den Entscheidungen:
- Frage 1 verwendet
sum()einer Maske, nichtlen(orders[...]). Beides ergibt 3, aber die Maske sagt „zählen“ direkt und baut keine Tabelle. - Frage 2 verwendet
!=für „außerhalb der Nordfiliale“.~(orders["branch"] == "north")bedeutet dasselbe;!=ist bei einem einzelnen Wert einfacher. - Frage 3 behält die ursprünglichen Labels
1und6, weil die Aufgabe keine Neu-Nummerierung verlangte. Diese Labels sagen dir, welche Zeilen vonorderses waren. - Frage 4 zählt, bevor sie schreibt, und der neue Wert wird aus denselben Zeilen auf der rechten Seite berechnet:
orders.loc[discount, "price"] * 0.9. Beide Seiten verwenden dieselbe Maske, sodass jede Zeile 90% ihres eigenen Preises erhält: 8,0 wird zu 7,2, 15,0 wird zu 13,5. Die Stiftbestellung1001(12 Artikel) und das Notizbuch (5 Artikel) behalten ihre Preise.
Und die drei Fehler:
- Ohne Klammern löst Frage 3
TypeError: Cannot perform 'rand_' …aus, weil&vor<angewendet wurde. orders[discount]["price"] = …gibt dieChainedAssignmentError-Warnung aus, das Skript läuft weiter, und die finale Tabelle zeigt noch immer die alten Preise8.0und15.0. Die Zeile hat nichts bewirkt.- Mit
"East"verliert Frage 3 still den Stift der Ostfiliale und gibt eine statt zwei Zeilen aus. Kein Fehler, keine Warnung: nur eine falsche Antwort, die richtig aussieht. Deshalb schreibt der Plan die erwarteten Zeilen auf, bevor der Code läuft. Das Einzige, was diesen Fehler aufdeckt, ist eine Person, die bereits wusste, dass das Ergebnis zwei Zeilen haben sollte.
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