Spalten und Zeilen auswählen — nach Name oder nach Platz
Auf Teile einer Tabelle zeigen: Spalten mit eckigen Klammern, Zeilen nach Label mit loc und nach Position mit iloc, einzelne Zellen mit at — und wissen, welche der beiden Arten du verwendest, denn nach einem Zuschnitt oder einer Sortierung stimmen sie nicht mehr überein.
Wie möchtest du lernen?
Das Problem, das wir lösen
Du hast die Bestelldatei des Shops eingelesen und überprüft, genau wie in Kapitel drei gezeigt. Jetzt fangen die Leute an, dir Fragen dazu zu stellen.
Das Lagerteam sagt: „Schick uns einfach das Produkt und die Menge jeder Bestellung — sonst nichts.“ Die Buchhaltung sagt: „Was genau war Bestellung 1006 und was hat sie gekostet?“ Und dein Manager sagt: „Zeig mir die letzten drei Bestellungen, die reingekommen sind.“
Keine dieser Fragen ist eine Berechnung über die gesamte Tabelle. Bei jeder einzelnen geht es darum, auf etwas zu zeigen: auf bestimmte Spalten, auf eine einzelne Zeile, auf die letzten paar Zeilen, auf eine einzelne Zelle. Alles auszugeben und mit dem Finger abzulesen funktioniert bei acht Zeilen. Bei achttausend funktioniert es nicht mehr, und in einem Programm funktioniert es gar nicht, denn ein Programm hat keinen Finger.
Pandas bietet mehrere Möglichkeiten, auf etwas zu zeigen, und der Grund für diese Vielfalt ist das eigentliche Thema dieses Kapitels. Auf eine Zeile kann auf zwei verschiedene Arten gezeigt werden: über ihren Namen (ihr Label im Index) oder über ihren Platz (erste, zweite, letzte). Wenn eine Tabelle frisch eingelesen wird, sind diese beiden zufällig dieselben Zahlen, sodass Anfänger gar nicht bemerken, dass es zwei verschiedene Dinge sind. Sobald du aber die letzten drei Zeilen nimmst, sortierst oder filterst, stimmen sie nicht mehr überein — und Code, der sie verwechselt hat, liefert plötzlich die falsche Zeile zurück, ohne einen einzigen Fehler auszulösen.
In diesem Kapitel geht es also um das Auswählen, vor allem aber darum, immer genau zu wissen, welche der beiden Arten du gerade verwendest.
Am Ende dieses Kapitels kannst du
- Eine Spalte auswählen und eine
Serieserhalten, oder mehrere und einDataFrameerhalten — und wissen, welches davon du gerade in den Händen hältst - Zeilen nach Label mit
.locund nach Position mit.ilocauswählen und erklären, warum ihre Slices unterschiedlich enden - Zeilen und Spalten gemeinsam mit
.loc[rows, columns]auswählen und die Form des Ergebnisses vorhersagen — bis hin zu einem einzelnen Wert mit.locoder.at - Zeilen mit
set_index()aussagekräftige Labels geben und sehen, wie sich dadurch die Bedeutung von.locverändert - Erklären, warum das Ändern einer Auswahl unter pandas 3 die Originaltabelle nicht verändert, und das Original korrekt ändern; die Fehlermeldungen von
KeyErrorundIndexError, die bei der Auswahl entstehen, lesen und beheben
Voraussetzungen: Eine CSV einlesen und prüfen, was angekommen ist.
Erstelle zuerst die Datei
Erstelle in deinem Projektordner die Datei orders.csv mit genau diesen neun Zeilen. Diese Datei wird für den Rest des Kurses verwendet, speichere sie also an einem Ort, an dem du sie wiederfindest:
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.0Jede Zeile ist eine Bestellung: eine Bestellnummer, das Datum, die Filiale des Shops, die sie verkauft hat, was verkauft wurde, die Kategorie des Produkts, wie viele und der Preis für ein Stück.
Bevor du den Code schreibst
Das Auswählen sieht so simpel aus, dass viele sofort anfangen, eckige Klammern zu tippen. Nimm dir zuerst eine Minute Zeit für diese Schritte, denn jeder Fehler beim Auswählen in diesem Kapitel entsteht dadurch, dass einer davon übersprungen wurde.
1. Formuliere die Frage in einem einzigen Satz und unterstreiche die Nomen. Die Nomen verraten dir, worauf du zeigst:
- „das Produkt und die Menge jeder Bestellung“ — zwei Spalten, alle Zeilen
- „Bestellung 1006“ — eine Zeile, benannt nach ihrer Bestellnummer
- „die letzten drei Bestellungen“ — Zeilen nach Position, gezählt vom Ende
- „der Preis von Bestellung 1006“ — eine Zelle: eine Zeile und eine Spalte zusammen
2. Schau dir die Tabelle an, bevor du hineinzeigst. Du kannst nur auf Namen zeigen, die tatsächlich existieren. Drei Dinge zählen bei der Auswahl: die Größe, die exakten Spaltennamen und der Index — die Labels ganz links.
import pandas as pd
orders = pd.read_csv("orders.csv")
print(orders.shape)
print(list(orders.columns))
print(orders.index)(8, 7)
['order_id', 'date', 'branch', 'product', 'category', 'quantity', 'price']
RangeIndex(start=0, stop=8, step=1)Die Spaltennamen werden als Liste in Anführungszeichen zurückgegeben — so siehst du direkt, dass 'price' kleingeschrieben ist und keine verirrten Leerzeichen enthält. Der Index ist ein RangeIndex von 0 bis 7: Die Labels sind schlicht eine Zählung. Merk dir das gut, denn genau um diesen Zufall dreht sich das gesamte Kapitel. Im Moment ist die Zeile mit dem Label 2 auch die Zeile an Position 2. Das wird nicht immer so bleiben.
3. Skizziere die Antwort, bevor du sie erzeugst. Was soll herauskommen — eine einzelne Zahl, eine Spalte oder eine kleinere Tabelle? Wie viele Zeilen und Spalten? Für die Anfrage des Lagerteams lautet die Antwort: eine Tabelle mit 8 Zeilen und 2 Spalten. Für „Bestellung 1006“ ist es eine einzelne Zeile. Für „der Preis von Bestellung 1006“ ist es eine einzelne Zahl, 850.0. Wenn du die erwartete Form kennst, fällt eine falsche Auswahl sofort auf, sobald du das Ergebnis siehst.
4. Wähle das Werkzeug nach dem aus, was du weißt. Diese Liste fasst das Kapitel auf einen Blick zusammen:
- Du kennst Spaltennamen → eckige Klammern:
orders["quantity"],orders[["product", "quantity"]] - Du kennst das Label einer Zeile, ihren Namen im Index →
.loc:orders.loc[2] - Du kennst die Position einer Zeile — erste, letzte, fünfte →
.iloc:orders.iloc[-3:] - Du brauchst Zeilen und Spalten gemeinsam →
.loc[rows, cols]oder.iloc[rows, cols]:orders.loc[2:4, ["product", "price"]] - Du brauchst genau eine Zelle →
.at[row, col]oder.loc[row, col]:orders.at[5, "price"]
5. Entscheide, wie du das Ergebnis überprüfst. Gib nach jeder Auswahl .shape oder type() des Ergebnisses aus und vergleiche es mit deiner Skizze. Es ist eine einzige Zeile, und sie fängt die meisten der unten genannten Fehler ab, bevor daraus falsche Zahlen werden.
Der Rest des Kapitels geht diese Werkzeuge nacheinander durch, jedes mit einem eigenen gelösten Beispiel.
Eine Spalte: Ein Name in eckigen Klammern
Setze einen Spaltennamen in eckige Klammern und du erhältst diese Spalte:
quantity = orders["quantity"]
print(type(quantity))
print(quantity)<class 'pandas.Series'>
0 12
1 5
2 2
3 7
4 30
5 1
6 20
7 4
Name: quantity, dtype: int64Was zurückkommt, ist eine Series — genau das Objekt aus Kapitel zwei: Werte, ein Index ganz links, ein Name (die Spalte, aus der sie stammt) und ein einzelner dtype. Beachte, dass der Index direkt mitgekommen ist. Die Zahlen 0 bis 7 sind keine Dekoration: Sie sagen immer noch aus, zu welcher Zeile jede Zahl gehört.
Da es sich um eine Series handelt, funktioniert jede Series-Methode direkt darauf:
print(orders["quantity"].sum())81Das ist der übliche Grund, eine einzelne Spalte auszuwählen: um eine Frage an sie zu stellen.
Mehrere Spalten: Eine Liste in den Klammern
Für das Lagerteam benötigst du zwei Spalten. Setze eine Liste von Namen in die Klammern:
stock = orders[["product", "quantity"]]
print(type(stock))
print(stock)<class 'pandas.DataFrame'>
product quantity
0 pen 12
1 notebook 5
2 bag 2
3 bottle 7
4 eraser 30
5 bag 1
6 pen 20
7 bottle 4Die doppelten eckigen Klammern sind keine Spezialsyntax, und es lohnt sich, das genau zu verstehen. Das äußere Paar ist die Auswahl; das innere Paar ist eine ganz normale Python-Liste, ["product", "quantity"]. Du könntest die Liste auch zuerst erstellen und dann übergeben — wanted = ["product", "quantity"], danach orders[wanted] — und es würde exakt dasselbe bedeuten. Die Spalten kommen in der Reihenfolge der Liste zurück, daher ist dies auch der Weg, um Spalten umzuordnen.
Das Ergebnis ist ein DataFrame, denn eine Tabelle mit zwei Spalten ist eine Tabelle. Und hier ist ein Unterschied, über den Entwickler monatelang stolpern: Eine Liste mit einem einzigen Namen darin liefert trotzdem ein DataFrame.
print(orders["product"].shape)
print(orders[["product"]].shape)(8,)
(8, 1)(8,) ist eine Series — eine Dimension, acht Werte. (8, 1) ist ein DataFrame mit acht Zeilen und einer Spalte. Sie werden unterschiedlich ausgegeben, sie haben unterschiedliche Methoden, und wenn du sie in einer Datei speicherst, behält das DataFrame seinen Spalten-Header. Die Regel ist einfach: Ein Name liefert eine Series; eine Liste liefert ein DataFrame — ganz gleich, wie viele Namen in der Liste stehen.
Warum nicht orders.quantity?
In vielen Tutorials wirst du sehen, dass auf Spalten mit einem Punkt zugegriffen wird:
print(orders.branch.head(2))0 north
1 south
Name: branch, dtype: strHier funktioniert es und es ist kürzer. Aber es funktioniert nur dann, wenn der Spaltenname zufällig ein gültiger Python-Name ist, der nicht bereits für etwas anderes verwendet wird — und ein DataFrame besitzt Hunderte von Methoden und Attributen. Hier ist eine kleine Tabelle, deren Spalten count und size heißen, zwei vollkommen verständliche Namen für einen Shop:
d = pd.DataFrame({"count": [1, 2], "size": [3, 4]})
print(d.size)
print(d["size"])4
0 3
1 4
Name: size, dtype: int64d.size hat dir überhaupt nicht die Spalte geliefert. Es hat dir das DataFrame-eigene size-Attribut zurückgegeben — die Anzahl der Zellen, 2 × 2 = 4 — und zwar still und leise. Kein Fehler, einfach nur eine falsche Antwort. Ein Name mit einem Leerzeichen, wie unit price, lässt sich mit einem Punkt gar nicht erst schreiben. Die eckigen Klammern haben keines dieser Probleme, daher lautet die Regel für Code, den du behalten willst: Verwende df["col"]. Der Punkt ist für einen schnellen Blick im Terminal in Ordnung, mehr aber auch nicht.
Zeilen nach Label: .loc
Zeilen werden nicht mit einfachen eckigen Klammern ausgewählt. (Einfache Klammern mit einem Namen bedeuten „Spalte“, und pandas wird nicht erraten, was du gemeint hast.) Zeilen bekommen ihre eigenen zwei Werkzeuge, und das erste ist .loc — label-based location. Du übergibst ihm das Label aus dem Index:
print(orders.loc[2])order_id 1003
date 2024-03-02
branch north
product bag
category accessories
quantity 2
price 850.0
Name: 2, dtype: objectEine einzelne Zeile kommt als Series zurück, die auf die Seite gekippt wurde: Die Spaltennamen sind zu ihrem Index geworden, und ihr Name ist das Label der Zeile, 2.
Schau dir den dtype: object an. Eine Spalte hat einen einzigen Typ, aber eine Zeile verläuft quer über alle Spalten hinweg — eine Zahl, etwas Text, eine Dezimalzahl —, sodass der einzige Typ, der alle zusammen aufnehmen kann, der allgemeine Typ object ist. Das ist einer der Gründe, warum pandas in Spalten denkt: Eine Zeile mischt Typen, eine Spalte nicht.
Jetzt kommt der wichtige Teil. Frage .loc nach einem Bereich von Labels:
print(orders.loc[2:4])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
4 1005 2024-03-03 north eraser stationery 30 8.0Drei Zeilen: 2, 3 und 4. Ein .loc-Slice schließt sein Ende mit ein. Das überrascht jeden, der Python kennt, wo range(2, 4) und list[2:4] vor der 4 stoppen. Das ist keine Inkonsistenz; es gibt einen guten Grund dafür.
Labels sind nicht immer Zahlen. Wenn die Labels Produktnamen wären, würdest du so etwas wie loc["bag":"eraser"] schreiben. Um das Ende auszuschließen, müsstest du das Label nach "eraser" benennen — und bei Namen gibt es kein „nächstes Label“, das man kennen könnte, ohne vorher nachzuschauen. Bei Labels nennt man also das erste, das man haben möchte, und das letzte, das man haben möchte, und man bekommt beide. Bei Positionen ist das anders: Die nächste Position nach 4 ist immer 5, daher kostet Pythons „Stopp davor“-Regel dort nichts.
Zeilen nach Position: .iloc
Das zweite Werkzeug ist .iloc — integer location. Es ignoriert die Labels vollständig und zählt Positionen ab 0, exakt wie eine Python-Liste:
print(orders.iloc[2:4])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.0Zwei Zeilen, Positionen 2 und 3; Position 4 wird ausgeschlossen, wie überall in Python. Stell die beiden Ausgaben nebeneinander: loc[2:4] lieferte drei Zeilen und iloc[2:4] lieferte zwei, auf derselben Tabelle, mit denselben Zahlen in den Klammern.
Da .iloc positionsbasiert ist, zählen negative Zahlen vom Ende — was die Frage deines Managers direkt beantwortet:
print(orders.iloc[-3:])order_id date branch product category quantity price
5 1006 2024-03-03 south bag accessories 1 850.0
6 1007 2024-03-04 east pen stationery 20 15.0
7 1008 2024-03-04 north bottle accessories 4 120.0„Die letzten drei“ ist eine Frage nach der Position, also ist .iloc das dazu passende Werkzeug. .loc hat keine Ahnung, was „letzte“ bedeutet; es kennt nur Namen.
Labels und Positionen sind nicht dasselbe
Bisher waren Label 2 und Position 2 dieselbe Zeile, sodass du die beiden Werkzeuge anhand ihrer Ergebnisse nicht unterscheiden konntest. Nimm die letzten drei Bestellungen als eigene Tabelle, und schon driften sie auseinander:
last3 = orders.tail(3)
print(last3)order_id date branch product category quantity price
5 1006 2024-03-03 south bag accessories 1 850.0
6 1007 2024-03-04 east pen stationery 20 15.0
7 1008 2024-03-04 north bottle accessories 4 120.0Die Zeilen haben ihre Labels behalten: 5, 6, 7. Pandas nummeriert sie mit Absicht nicht neu um — das Label ist der Weg, wie eine Zeile dieselbe Zeile bleibt, nachdem du die Tabelle zugeschnitten hast. Frage nun auf beide Arten nach „der ersten Zeile“:
print(last3.iloc[0])order_id 1006
date 2024-03-03
branch south
product bag
category accessories
quantity 1
price 850.0
Name: 5, dtype: objectlast3.loc[0]KeyError: 0.iloc[0] bedeutet „die Zeile an erster Stelle“, was der Bestellung mit dem Label 5 entspricht. .loc[0] bedeutet „die Zeile, deren Label 0 ist“ — und eine solche Zeile gibt es in dieser Tabelle nicht, weshalb pandas einen KeyError auslöst. Das ist genau der Fehler, der einem später im Kurs zum Verhängnis wird: Nachdem du Zeilen gefiltert oder sortiert hast — beides kommt später im Kurs —, ändern sich die Positionen, die Labels jedoch nicht. Die Regel, die du dir merken solltest:
- Fragst du nach dem Platz — erste, letzte, die ersten drei nach dem Sortieren? Verwende
.iloc. - Fragst du nach der Identität — diese Bestellung, dieses Produkt? Verwende
.loc.
Und der schlimme Fall ist nicht der KeyError. Der schlimme Fall tritt ein, wenn das Label 0 an anderer Stelle in der Tabelle tatsächlich existiert: Dann liefert .loc[0] still und leise eine ganz andere Zeile zurück als die, die du eigentlich gemeint hast.
.locfragt nach einem Namen,.ilocfragt nach einem Platz. Bei einer frisch eingelesenen Tabelle stimmen beide rein zufällig überein, sodass Code, der sie verwechselt, jeden Test besteht, den du am ersten Tag ausführst — und die falsche Zeile ohne jede Fehlermeldung zurückgibt, sobald die Tabelle zum ersten Mal zugeschnitten, gefiltert oder sortiert wurde.
Zeilen aussagekräftige Labels geben: set_index
Die Buchhaltung fragte nach Bestellung 1006. Beim Standardindex müsstest du wissen, dass Bestellung 1006 unter Label 5 liegt — ein Detail über die Datei, nicht über die Bestellung. Der eigentliche Name einer Zeile ist ihre Bestellnummer, also mache die Bestellnummer zum Index:
by_id = orders.set_index("order_id")
print(by_id.head(3))
print(by_id.shape)date branch product category quantity price
order_id
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
(8, 6)Drei Dinge haben sich geändert. Die Bestellnummern sind nach links in den Index gewandert. Der Index hat nun einen Namen, order_id, der in einer eigenen Zeile darüber ausgegeben wird. Und die Form ist (8, 6) statt (8, 7) — order_id ist keine Spalte mehr, sondern der Index. Beachte auch, dass set_index eine neue Tabelle zurückgegeben hat, die wir by_id genannt haben; orders selbst bleibt unverändert.
Jetzt spricht .loc die Sprache der Fragestellung:
print(by_id.loc[1006])date 2024-03-03
branch south
product bag
category accessories
quantity 1
price 850.0
Name: 1006, dtype: objectUnd ein Label-Slice ist nun ein Bereich von Bestellnummern, bei dem beide Enden eingeschlossen sind:
print(by_id.loc[1003:1005, ["product", "quantity"]])product quantity
order_id
1003 bag 2
1004 bottle 7
1005 eraser 30.iloc hat sich überhaupt nicht verändert — Positionen bleiben Positionen, ganz egal, wie die Labels lauten:
print(by_id.iloc[0:2])date branch product category quantity price
order_id
1001 2024-03-01 north pen stationery 12 15.0
1002 2024-03-01 south notebook stationery 5 60.0Hier haben sich die beiden Werkzeuge klar getrennt: by_id.loc[0] wäre jetzt ein KeyError, weil 0 keine Bestellnummer ist, während by_id.iloc[0] Bestellung 1001 liefert. Falls du die Bestellnummer jemals wieder als Spalte zurückhaben möchtest, bringt by_id.reset_index() sie wieder dorthin.
Wähle eine Spalte, deren Werte eindeutig sind. Ein Index soll Zeilen benennen, und ein Name, den sich zwei Zeilen teilen, zeigt nicht eindeutig auf eine einzelne. Sieh dir an, was mit product passiert, das mehrfach vorkommt:
by_product = orders.set_index("product")
print(by_product.index.is_unique)
print(by_product.loc["bag"])False
order_id date branch category quantity price
product
bag 1003 2024-03-02 north accessories 2 850.0
bag 1006 2024-03-03 south accessories 1 850.0Kein Fehler — aber loc["bag"] hat ein zweizeiliges DataFrame statt einer einzelnen Zeile als Series zurückgegeben. Code, der eine einzelne Zeile erwartet hat (um etwa deren price als einzelne Zahl zu lesen), erhält nun zwei. Genau deshalb lohnt sich eine Zeile mit index.is_unique zur Überprüfung, wann immer du einen Index setzt, nach dem du Zeilen nachschlagen willst.
Zeilen und Spalten gemeinsam
Sowohl .loc als auch .iloc nehmen zwei durch ein Komma getrennte Angaben entgegen: Zeilen zuerst, Spalten an zweiter Stelle. Jeder Teil kann ein einzelnes Label, eine Liste oder ein Slice sein:
print(orders.loc[2:4, ["product", "price"]])product price
2 bag 850.0
3 bottle 120.0
4 eraser 8.0print(orders.loc[[0, 3], ["branch", "product"]])branch product
0 north pen
3 east bottleEin Slice funktioniert auch für Spalten, mit der einschließenden Regel von .loc — „jede Spalte von branch bis category“. Ein einzelner Doppelpunkt : bedeutet „alle Zeilen“:
print(orders.loc[:, "branch":"category"])branch product category
0 north pen stationery
1 south notebook stationery
2 north bag accessories
3 east bottle accessories
4 north eraser stationery
5 south bag accessories
6 east pen stationery
7 north bottle accessories.iloc macht dasselbe über Positionen. Auch Spalten werden gezählt, ab 0, sodass branch Spalte 2 und product Spalte 3 ist:
print(orders.iloc[[0, 3], [2, 3]])branch product
0 north pen
3 east bottleDasselbe Ergebnis wie bei der .loc-Variante zwei Blöcke weiter oben — aber beachte, wie viel schwerer es zu lesen ist. [2, 3] sagt gar nichts aus; ["branch", "product"] sagt alles aus und funktioniert auch dann noch, wenn jemand ganz vorne in der Datei eine Spalte einfügt. Bevorzuge Namen, wenn du die Namen kennst.
Welche Form kommt zurück?
Ob du einen einzelnen Wert, eine Series oder ein DataFrame zurückbekommst, hängt einzig davon ab, was du auf die jeweilige Seite des Kommas setzt. Ein einzelnes Label entfernt diese Dimension; eine Liste oder ein Slice behält sie bei:
print(type(by_id.loc[1006, "price"]).__name__)
print(type(by_id.loc[1006, ["product", "price"]]).__name__)
print(type(by_id.loc[[1003, 1006], "price"]).__name__)
print(type(by_id.loc[[1003, 1006], ["product", "price"]]).__name__)float64
Series
Series
DataFrame- ein Label, ein Label → ein einzelner Wert
- ein Label, eine Liste oder ein Slice → eine
Series(eine Zeile) - eine Liste oder ein Slice, ein Label → eine
Series(eine Spalte) - eine Liste oder ein Slice, eine Liste oder ein Slice → ein
DataFrame
Das ist Schritt 3 der Planung — skizziere die Antwort —, formuliert als feste Regel. Wenn du eine Tabelle zurückhaben willst, selbst bei nur einer einzigen Zeile, setze auf beide Seiten eine Liste: by_id.loc[[1006], ["price"]].
Ein einzelner Wert: .loc und .at
Die Frage der Buchhaltung — was hat Bestellung 1006 gekostet? — ist eine einzelne Zelle: ein Zeilenlabel und ein Spaltenname.
print(by_id.loc[1006, "price"])
print(by_id.at[1006, "price"])850.0
850.0Beide liefern dieselbe Zahl. .at akzeptiert ausschließlich eine einzelne Zeile und eine einzelne Spalte, sonst nichts. Das macht es geringfügig schneller und verdeutlicht deine Absicht: Das hier ist eine einzelne Zelle. (.iat ist das positionsbasierte Gegenstück, by_id.iat[5, 5].) Verwende das, was sich für dich besser liest; .loc ist völlig in Ordnung.
Ein einzelner Wert ist eine normale Zahl, sodass du direkt damit rechnen kannst. Der Gesamtwert von Bestellung 1004 — Menge mal Preis:
print(by_id.at[1004, "quantity"] * by_id.at[1004, "price"])840.0Eine Auswahl ist ein eigenes Objekt
Es gibt noch eine Sache zu verstehen, bevor du Auswahlen in echten Programmen verwendest: was passiert, wenn du eine Auswahl veränderst.
import pandas as pd
orders = pd.read_csv("orders.csv")
prices = orders["price"]
prices[0] = 999.0
print(prices.head(2))
print(orders.loc[0, "price"])0 999.0
1 60.0
Name: price, dtype: float64
15.0prices hat sich geändert. orders nicht. In pandas 3 verhält sich jede Auswahl wie eine separate Kopie: Du kannst sie nach Belieben verändern, und die Originaltabelle wird niemals angetastet. (Intern vermeidet pandas das tatsächliche Kopieren der Daten, bis du hineinschreibst — das ist der Name des Mechanismus: Copy-on-Write —, aber die Regel, die du brauchst, ist das Verhalten, nicht der Mechanismus.)
Dasselbe gilt für eine Auswahl mehrerer Spalten:
stock = orders[["product", "price"]]
stock["price"] = 0
print(orders["price"].head(2))0 15.0
1 60.0
Name: price, dtype: float64Das ist das sichere Verhalten: Eine Funktion, die eine Auswahl entgegennimmt und verändert, kann deine Haupttabelle nicht hinter deinem Rücken beschädigen. Aber das hat eine Konsequenz, die du kennen musst. Wenn du beabsichtigst, das Original zu ändern, musst du das direkt am Original tun, in einem einzigen Schritt, mit .loc:
orders.loc[0, "price"] = 16.0
print(orders.loc[0, "price"])16.0Die verlockende zweistufige Variante — erst eine Spalte auswählen, dann eine Zelle darin setzen — funktioniert nicht, und pandas 3 teilt dir das unmissverständlich mit:
orders["price"][0] = 1.0ChainedAssignmentError: 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-assignmentprint(orders.loc[0, "price"])16.0Immer noch 16.0 — die Zuweisung landete in einer temporären Kopie und wurde verworfen. orders["price"] erzeugte eine Auswahl, und [0] = 1.0 änderte diese, nicht orders. Das nennt man chained assignment (verkettete Zuweisung): zwei eckige Klammern nacheinander links vom =. Die Reparatur ist immer diejenige, die in der Warnung genannt wird: ein einzelnes .loc mit sowohl der Zeile als auch der Spalte, orders.loc[0, "price"] = 1.0.
Wenn du ältere Tutorials liest, wirst du dieses Thema mit einer SettingWithCopyWarning und dem Hinweis beschrieben sehen, dass sich das Original manchmal geändert hat und manchmal nicht. Das war pandas 1 und 2. In pandas 3 ist die Regel einfach und immer dieselbe: Eine Auswahl ändert niemals das Original; ein .loc auf dem Original tut es.
Ein vollständiges Beispiel
Alle drei Anfragen vom Anfang des Kapitels, beantwortet in einem einzigen Programm. select_orders.py:
import pandas as pd
orders = pd.read_csv("orders.csv")
# 1. Check what arrived before pointing into it
print("Shape :", orders.shape)
print("Columns:", list(orders.columns))
# 2. Name the rows by what they are
by_id = orders.set_index("order_id")
print("Order ids unique:", by_id.index.is_unique)
print()
# Stock team: two columns, every row
stock = orders[["product", "quantity"]]
print("Stock view:", stock.shape)
print(stock.head(3))
print()
# Accountant: one order by its number, then one cell
print(by_id.loc[1006, ["product", "quantity", "price"]])
print("Order 1006 total:", by_id.at[1006, "quantity"] * by_id.at[1006, "price"])
print()
# Manager: the last three orders, a question about position
print(by_id.iloc[-3:, [1, 2, 4]])Shape : (8, 7)
Columns: ['order_id', 'date', 'branch', 'product', 'category', 'quantity', 'price']
Order ids unique: True
Stock view: (8, 2)
product quantity
0 pen 12
1 notebook 5
2 bag 2
product bag
quantity 1
price 850.0
Name: 1006, dtype: object
Order 1006 total: 850.0
branch product quantity
order_id
1006 south bag 1
1007 east pen 20
1008 north bottle 4Warum es so geschrieben ist:
- Es schaut hin, bevor es hineinzeigt.
shapeundlist(columns)werden zuerst ausgegeben, sodass ein falsch geschriebener oder mit Leerzeichen versehener Spaltenname sichtbar wird, bevor irgendeine Auswahl fehlschlägt. - Es stellt sicher, dass der Index eindeutig ist, bevor es ihn verwendet, um Bestellungen nachzuschlagen. So ist garantiert, dass
by_id.loc[1006]genau eine Zeile undby_id.at[...]genau eine Zahl liefert. - Jede Anfrage verwendet das Werkzeug, das zu ihrer Fragestellung passt. Spalten nach Namen mit eckigen Klammern; eine Bestellung nach ihrer Identität mit
.locund.at; „die letzten drei“ nach Position mit.iloc. - Es gibt die Form der Lageransicht aus —
(8, 2)entspricht exakt der Skizze: jede Bestellung, zwei Spalten. - Die letzte Auswahl ist diejenige, die man verbessern sollte.
[1, 2, 4]steht inby_idzufällig fürbranch,productundquantity— beachte, dass sich die Positionen um eins verschoben haben, weilorder_idzum Index wurde. Positionen sind fragil. In deinem eigenen Code würdenby_id.iloc[-3:][["branch", "product", "quantity"]]oderby_id.loc[by_id.index[-3:], ["branch", "product", "quantity"]]mit Namen genau dasselbe ausdrücken.
Wenn etwas schiefgeht
KeyError: 'Price' Es gibt keine Spalte mit exakt diesem Namen. Spaltennamen unterscheiden zwischen Groß- und Kleinschreibung: price und Price sind unterschiedliche Namen. Gib list(df.columns) aus und kopiere den Namen von dort. Bei einer Liste von Namen, orders[["product", "Price"]], lautet die Meldung KeyError: "['Price'] not in index" und nennt genau denjenigen, der fehlt; mit einem Punkt, orders.Price, lautet sie AttributeError: 'DataFrame' object has no attribute 'Price'. Gleiche Ursache, gleiche Behebung.
KeyError: 'price' — obwohl price eindeutig in der Datei steht Der Header hat wahrscheinlich Leerzeichen nach seinen Kommas, order_id, product, price, und diese Leerzeichen wurden Teil der Namen:
from io import StringIO
import pandas as pd
raw = "order_id, product, price\n1001, pen, 15.0\n"
padded = pd.read_csv(StringIO(raw))
print(list(padded.columns))
padded["price"]['order_id', ' product', ' price']
KeyError: 'price'Die Liste zeigt das führende Leerzeichen in ' price', was genau der Grund ist, warum dir Kapitel drei geraten hat, die Spalten als Liste auszugeben. Behebe es direkt beim Einlesen mit pd.read_csv("file.csv", skipinitialspace=True) oder danach mit df.columns = df.columns.str.strip().
KeyError: ('product', 'price') Du hast orders["product", "price"] geschrieben — ein einzelnes Paar eckiger Klammern. Ohne die innere Liste übergibt Python die beiden Namen als einzelnes Tupel, und pandas sucht nach einer einzelnen Spalte namens ('product', 'price'). Schreibe stattdessen orders[["product", "price"]].
KeyError: 0 von .loc Du hast eine Position mit .loc verwendet. Entweder wurde die Tabelle zugeschnitten (tail, ein Filter, eine Sortierung), sodass das Label 0 nicht mehr existiert, oder du hast einen Index gesetzt und die Labels sind jetzt Bestellnummern. Wenn du die „erste Zeile“ meinst, schreibe .iloc[0]. Bei einer Tabelle, deren Index aus Text besteht, löst derselbe Fehler bei einem Slice — orders.set_index("product").loc[0:2] — stattdessen TypeError: cannot do slice indexing on Index with these indexers [0] of type int aus; die Lösung ist dieselbe: .iloc[0:2].
IndexError: single positional indexer is out-of-bounds .iloc wurde eine Position übergeben, die nicht existiert — orders.iloc[8] bei einer achtzeiligen Tabelle, deren Positionen von 0 bis 7 reichen. Überprüfe df.shape; die letzte Zeile lautet immer df.iloc[-1]. Dieselbe Meldung erscheint bei einer zu großen Spaltenposition, etwa orders.iloc[:, 7].
ValueError: Location based indexing can only have [integer, integer slice (START point is INCLUDED, END point is EXCLUDED), listlike of integers, boolean array] types Du hast .iloc einen Spaltennamen übergeben: orders.iloc[0, "branch"]. .iloc akzeptiert nur Positionen. Verwende .loc mit einem Label — orders.loc[0, "branch"] — oder ermittle die Position, falls du sie wirklich brauchst: orders.columns.get_loc("branch") gibt 2 zurück.
Schritt 4 von 6 — Vorhersagen
Prüfe dein Verständnis
Dieselbe Tabelle, dieselben Zahlen in den Klammern. 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))
print(len(orders.loc[2:5]), len(orders.iloc[2:5]))- A4 4
- B3 4
- C3 3
- D4 3
Das hier soll das Produkt der ersten der letzten drei Bestellungen ausgeben. Was passiert tatsächlich und warum?
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))
last3 = orders.tail(3)
# Wanted: the product of the first of these three orders
print(last3.loc[0, "product"])- AEs gibt `bag` aus, das Produkt der ersten der drei Zeilen
- B`IndexError` — die Tabelle hat nur drei Zeilen
- C`KeyError: 0` — `tail(3)` hat die Labels 5, 6 und 7 behalten, und `.loc` sucht nach einem Label 0
- DEs gibt `pen` aus, das Produkt in der ersten Zeile der Originaltabelle
Du benötigst ein DataFrame, das nur die price-Spalte enthält — keine Series. Welche Zeile liefert dir das?
- Aorders.loc[:, "price"]
- Borders.price
- Corders["price"]
- Dorders[["price"]]
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
Schreibe unter Verwendung derselben orders.csv das Skript select.py. Schreibe vor jedem Codeblock in einem Kommentar auf, was jede Auswahl zurückgeben sollte — einen einzelnen Wert, eine Series oder ein DataFrame — und wie viele Zeilen. Das Programm muss vier Bedingungen erfüllen:
- Spalten werden immer nach Namen ausgewählt: Gib den
shapevondate,branchundquantityzusammen aus. - Bestellungen werden nach Identität auf einer nach
order_idindexierten Tabelle nachgeschlagen, nachdem geprüft wurde, dass der Index eindeutig ist: Gib die gesamte Zeile für Bestellung 1007 aus, danachproductundbranchfür die Bestellungen 1002 bis 1005 — alle vier. - „Erste“ und „letzte“ werden nach Position beantwortet: Gib
productundpricefür die ersten zwei und die letzten zwei Bestellungen aus; danach mit.atdie Menge von Bestellung 1005 und den Gesamtwert von Bestellung 1003 (Menge × Preis). - Ändere den Preis von Bestellung 1001 in der indexierten Tabelle mit einem einzigen
.locauf16.0und gib ihn aus, um zu beweisen, dass die Änderung stattgefunden hat.
Deine Ausgabe sollte exakt so aussehen:
(8, 3)
unique: True
date 2024-03-04
branch east
product pen
category stationery
quantity 20
price 15.0
Name: 1007, dtype: object
product branch
order_id
1002 notebook south
1003 bag north
1004 bottle east
1005 eraser north
product price
order_id
1001 pen 15.0
1002 notebook 60.0
product price
order_id
1007 pen 15.0
1008 bottle 120.0
qty of 1005: 30
total of 1003: 1700.0
price of 1001: 16.0Sage dann voraus, schreibe deine Vorhersage auf und führe erst danach jede dieser drei Zeilen aus:
by_id.loc[0]orders.iloc[8]by_id["price"][1001] = 99.0— und gib danach den Preis von Bestellung 1001 erneut aus
Lösung
Plane zuerst. Der Kommentarblock ist die Skizze aus Schritt 3 von „Bevor du den Code schreibst“ — was jede Auswahl zurückgeben sollte:
- Drei Spalten →
orders[[...]]→ einDataFramemit der Form(8, 3) - Eine Bestellung nach Identität →
by_id.loc[1007]→ eineSeries(eine Zeile) - Vier Bestellungen nach Identität, zwei Spalten →
by_id.loc[1002:1005, [...]]→ ein 4 × 2DataFrame, weil.locdie 1005 einschließt - Die ersten zwei und letzten zwei nach Platz →
.iloc[:2]und.iloc[-2:]→ zwei 2 × 2DataFrames - Einzelne Zellen →
.at→ einfache Zahlen - Das Original ändern → ein einzelnes
.locaufby_id→ die Tabelle selbst ändert sich
select.py:
import pandas as pd
orders = pd.read_csv("orders.csv")
# Columns by name -> a DataFrame of 8 rows, 3 columns
print(orders[["date", "branch", "quantity"]].shape)
# Rows named by their order number; check the names are unique first
by_id = orders.set_index("order_id")
print("unique:", by_id.index.is_unique)
# One order by identity -> a Series; four orders -> 4 x 2 (loc includes 1005)
print(by_id.loc[1007])
print(by_id.loc[1002:1005, ["product", "branch"]])
# First two and last two by place -> .iloc for rows, names for columns
print(by_id.iloc[:2][["product", "price"]])
print(by_id.iloc[-2:][["product", "price"]])
# Single cells -> plain numbers
print("qty of 1005:", by_id.at[1005, "quantity"])
print("total of 1003:", by_id.at[1003, "quantity"] * by_id.at[1003, "price"])
# Change the original, in one step
by_id.loc[1001, "price"] = 16.0
print("price of 1001:", by_id.at[1001, "price"])(8, 3)
unique: True
date 2024-03-04
branch east
product pen
category stationery
quantity 20
price 15.0
Name: 1007, dtype: object
product branch
order_id
1002 notebook south
1003 bag north
1004 bottle east
1005 eraser north
product price
order_id
1001 pen 15.0
1002 notebook 60.0
product price
order_id
1007 pen 15.0
1008 bottle 120.0
qty of 1005: 30
total of 1003: 1700.0
price of 1001: 16.0Jedes Ergebnis entspricht dem Plan: (8, 3) für drei Spalten, vier Bestellungen für 1002:1005, weil .loc sein Ende einschließt, und jeweils zwei Zeilen für die positionsbasierten Auswahlen. In Bedingung 3 werden die Zeilen nach Position und die Spalten nach Name ausgewählt — .iloc für die Zeilen, dann eine Liste von Namen —, sodass der Code genau ausdrückt, was er meint.
Nun zu den drei Vorhersagen:
by_id.loc[0]KeyError: 0Die Labels sind jetzt Bestellnummern; es gibt keine Bestellung 0. Die erste Zeile nach Position wäre by_id.iloc[0].
orders.iloc[8]IndexError: single positional indexer is out-of-boundsAcht Zeilen bedeuten Positionen von 0 bis 7. Die letzte Zeile ist orders.iloc[7] oder besser orders.iloc[-1].
by_id["price"][1001] = 99.0ChainedAssignmentError: 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-assignmentprint(by_id.at[1001, "price"])16.0Immer noch 16.0: by_id["price"] erzeugte eine separate Auswahl und die 99.0 landete darin, nicht in by_id. Bedingung 4 hat funktioniert und das hier nicht, und der Unterschied liegt in einem einzigen .loc gegenüber zwei eckigen Klammern nacheinander.
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