DataFrames kombinieren — stapeln mit concat, abgleichen mit merge
Tabellen zusammenführen: Monate derselben Daten mit concat stapeln und Informationen aus einer anderen Tabelle mit merge hinzufügen — die passende Merge-Art wählen, nicht übereinstimmende Zeilen erkennen und doppelte Schlüssel abfangen, die Zeilen stillschweigend vervielfachen.
Wie möchtest du lernen?
Das Problem, das wir lösen
Die Daten eines Geschäfts liegen nie in einer einzigen Datei. Wirklich nie.
Die März-Bestellungen befinden sich in orders.csv, der Tabelle, die du seit Kapitel vier verwendest. Die Bestellungen für den April sind diese Woche als separater Export eingetroffen: orders_april.csv. Was jedes Produkt das Geschäft im Einkauf kostet — und welcher Lieferant es verkauft —, verwaltet das Einkaufsteam in einer eigenen Datei, products.csv. Niemand hat die Kosten in die Bestellungsdatei geschrieben, weil Kosten eine Eigenschaft des Produkts sind und nicht der Bestellung.
Nun stellt die Ladenbesitzerin eine Frage: Wie viel Gewinn hat jeder Lieferant im März und April eingebracht?
Keine einzelne Datei kann das beantworten. Für den Gewinn benötigst du die Menge und den Verkaufspreis (in den Bestellungen), die Kosten (in der Produktdatei) und den Lieferanten (ebenfalls in der Produktdatei). Und die Bestellungen sind auf zwei Monate verteilt.
In einer Tabellenkalkulation würdest du die Zeilen des Aprils unter die des März kopieren, dann in einer neuen Spalte ein VLOOKUP schreiben, um die Kosten für jede Zeile heranzuziehen, die Formel nach unten ziehen und hoffen. Pandas erledigt dieselben beiden Aufgaben in zwei Zeilen:
- Tabellen mit derselben Art von Zeilen — März und April — zu einer längeren Tabelle stapeln:
pd.concat - Jede Zeile einer Tabelle über eine gemeinsame Spalte mit der passenden Zeile einer anderen abgleichen — jede Bestellung mit ihrem Produkt — und die Spalten der anderen Tabelle herüberholen:
pd.merge
Die Codezeilen sind kurz. Warum dieses Kapitel so entscheidend ist, liegt an den Dingen, die ganz ohne Fehlermeldung schiefgehen können: Ein Merge kann stillschweigend Bestellungen verwerfen, deren Produkt in der Lookup-Tabelle fehlt, oder Bestellungen stillschweigend duplizieren, wenn die Lookup-Tabelle ein Produkt doppelt aufführt. Beides liefert eine Gesamtsumme, die vollkommen plausibel aussieht und doch falsch ist. Der Großteil dieses Kapitels dreht sich daher darum, vor und nach dem Merge genau zu wissen, wie viele Zeilen du haben solltest — und das auch zu überprüfen.
Am Ende dieses Kapitels kannst du
- Die beiden Operationen unterscheiden: Zeilen stapeln (
concat) und anhand eines Schlüssels abgleichen (merge) - Tabellen mit
pd.concatstapeln, den wiederholten Index mitignore_index=Truebeheben und sehen, was nicht übereinstimmende Spalten bewirken - Zwei Tabellen über einen Schlüssel mit
pd.mergezusammenführen und gezielt zwischenhow="inner","left","right"oder"outer"wählen - Einen Merge mit
indicator=Trueprüfen und mitvalidate="many_to_one"absichern - Eine Zeilenexplosion durch doppelte Schlüssel erkennen und beheben
- Über unterschiedlich benannte Schlüssel mit
left_on/right_onmergen und kollidierende Spaltennamen mitsuffixeshandhaben - Die typischen Merge-Fehler erkennen — nicht übereinstimmende Schlüsseltypen, fehlende Schlüsselspalten — sowie die stillen Fehler, die gar keine Exception auslösen
Voraussetzungen: Spalten hinzufügen.
Erstelle zuerst die Dateien
Dieses Kapitel verwendet vier kleine Dateien. Lege sie neben deinem Skript ab.
orders.csv — März, dieselbe Datei wie in den vorherigen Kapiteln:
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.0orders_april.csv — April, dieselben Spalten. Beachte das Heftgerät: Das Geschäft hat im April damit begonnen, es zu verkaufen.
order_id,date,branch,product,category,quantity,price
1009,2024-04-01,north,pen,stationery,15,15.0
1010,2024-04-01,east,notebook,stationery,3,60.0
1011,2024-04-02,south,stapler,stationery,6,95.0
1012,2024-04-02,north,bag,accessories,1,850.0products.csv — eine Zeile pro Produkt, gepflegt vom Einkauf. Zwei Dinge sind hier ganz bewusst „falsch“, wie es auch im echten Leben der Fall wäre: Das neue Heftgerät wurde noch nicht hinzugefügt, und es gibt einen Textmarker, den das Geschäft noch nie verkauft hat.
product,cost,supplier
pen,9.0,Alpha
notebook,40.0,Alpha
bag,600.0,Bravo
bottle,80.0,Bravo
eraser,5.0,Alpha
marker,25.0,Alphabranches.csv — wer welche der drei Filialen des Geschäfts leitet. Die Spalte heißt branch_name und nicht branch, weil eine andere Person diese Datei erstellt hat.
branch_name,manager
north,Mira
south,Omar
east,LenaBevor du den Code schreibst
Das Kombinieren von Tabellen ist der Schritt, bei dem eine schnell getippte Codezeile am häufigsten zu einem vollkommen überzeugend wirkenden, aber falschen Ergebnis führt. Daher kommt die Planung zuerst — und sie besteht größtenteils aus Fragen, die du durch reines Hinsehen beantwortest.
1. Formuliere die Frage in einem einzigen Satz
„Gewinn pro Lieferant, März und April zusammen.“ Dieser Satz verrät dir bereits die finale Tabelle: eine Zeile pro Lieferant, eine Zahl pro Zeile. Es gibt zwei Lieferanten, Alpha und Bravo, also sollte das Ergebnis ungefähr zwei Zeilen haben. Wenn es am Ende drei oder nur eine Zeile sind, ist unterwegs etwas schiefgegangen.
2. Entscheide, welche Operation jedes Tabellenpaar benötigt
Es gibt nur zwei Arten des Kombinierens, und die Faustregel ist einfach: Enthalten die beiden Tabellen dieselbe Art von Zeilen oder unterschiedliche Fakten über dieselbe Sache?
- Dieselben Spalten, unterschiedliche Zeilen — März-Bestellungen + April-Bestellungen. Verwende
pd.concat([a, b]). Das Ergebnis wächst nach unten: mehr Zeilen, dieselben Spalten. - Eine gemeinsame Schlüsselspalte, unterschiedliche Fakten — Bestellungen + Produkte, verknüpft über
product. Verwendepd.merge(a, b, on="product"). Das Ergebnis wächst zur Seite: dieselben Zeilen, mehr Spalten.
März und April sind beides „Bestellungen“ — dieselben Spalten, mehr Zeilen —, also werden sie gestapelt. Bestellungen und Produkte sind unterschiedliche Dinge, die über den Produktnamen verknüpft sind, also werden sie abgeglichen.
3. Sieh dir jede Tabelle an, bevor du sie kombinierst
Du hast in Kapitel drei gelernt, eine Datei sofort nach dem Einlesen zu überprüfen. Bei mehreren Dateien ist diese Gewohnheit unverzichtbar: Du benötigst die Form (shape) jeder einzelnen Tabelle, um die Form des Ergebnisses vorherzusagen.
import pandas as pd
march = pd.read_csv("orders.csv")
april = pd.read_csv("orders_april.csv")
products = pd.read_csv("products.csv")
for name, df in [("march", march), ("april", april), ("products", products)]:
print(name, df.shape, list(df.columns))march (8, 7) ['order_id', 'date', 'branch', 'product', 'category', 'quantity', 'price']
april (4, 7) ['order_id', 'date', 'branch', 'product', 'category', 'quantity', 'price']
products (6, 3) ['product', 'cost', 'supplier']März und April haben exakt dieselben sieben Spalten in derselben Reihenfolge — perfekt zum Stapeln. products teilt genau einen Spaltennamen mit ihnen: product. Das ist der Schlüssel, die Spalte, über die der Abgleich stattfinden wird.
Prüfe, ob der Schlüssel auf beiden Seiten denselben Datentyp hat. Ein Schlüssel, der in einer Tabelle eine Zahl und in der anderen Text ist, kann niemals übereinstimmen:
print(march["product"].dtype, products["product"].dtype)str strBeide str. Sie können verglichen werden.
4. Prüfe, ob der Schlüssel auf der Lookup-Seite eindeutig ist
Dies ist die wichtigste Prüfung im gesamten Kapitel. Jede Bestellung benötigt einen Einkaufspreis. Wenn products.csv den Eintrag pen zweimal auflistet, wird jede pen-Bestellung zweimal zugeordnet und erscheint zweimal im Ergebnis.
print(products["product"].is_unique)
print(products["product"].duplicated().sum())True
0is_unique ist True und es gibt null Duplikate: eine Zeile pro Produkt. Jede Bestellung kann maximal mit einer einzigen Produktzeile übereinstimmen.
5. Prüfe, welche Schlüssel keinen Partner finden
Frage dich vor dem Zusammenführen: Gibt es Bestellungen, deren Produkt in products.csv fehlt, und Produkte, die niemand bestellt hat? isin (aus dem Kapitel über das Filtern) beantwortet beides:
orders = pd.concat([march, april], ignore_index=True)
no_cost = ~orders["product"].isin(products["product"])
print(orders.loc[no_cost, ["order_id", "product"]])
never_sold = ~products["product"].isin(orders["product"])
print(products.loc[never_sold, "product"].tolist())order_id product
10 1011 stapler
['marker']Eine Bestellung — das Heftgerät, 1011 — hat keine hinterlegten Kosten. Ein Produkt, der Textmarker, wurde nie verkauft. Jetzt weißt du vor dem Zusammenführen, worauf der Merge treffen wird.
6. Sage das Ergebnis voraus und entscheide dann
Schreibe die Vorhersage auf, in konkreten Zahlen:
ordershat 8 + 4 = 12 Zeilen.productshat eine Zeile pro Produkt, der Abgleich kann Bestellungen also nicht vervielfachen.- Wenn jede Bestellung behalten wird, hat die zusammengeführte Tabelle 12 Zeilen, und bei einer davon (dem Heftgerät) fehlen die Kosten.
- Wenn nur übereinstimmende Bestellungen behalten werden, sind es 11.
Was davon möchtest du? Für einen Gewinnbericht würde das stillschweigende Weglassen der Heftgerät-Bestellung den Umsatz zu niedrig ausweisen. Es ist besser, sie zu behalten, zu sehen, dass ihre Kosten fehlen, und dann zu entscheiden, was zu tun ist — genau das ist die Entscheidung, die du mit dem Argument how= triffst. Nach dem Merge ist das Allererste, was du ausgibst, die Shape der Tabelle, und du vergleichst sie mit dieser Vorhersage.
Das ist der gesamte Plan: Ein-Satz-Frage, Operation pro Tabellenpaar, jede Tabelle ansehen, Eindeutigkeit des Schlüssels, nicht abgeglichene Schlüssel, vorhergesagte Zeilenanzahl. Der Rest dieses Kapitels widmet sich den Werkzeugen, mit denen du diesen Plan umsetzt.
Zeilen stapeln mit pd.concat
pd.concat nimmt eine Liste von Tabellen entgegen und platziert sie untereinander:
import pandas as pd
march = pd.read_csv("orders.csv")
april = pd.read_csv("orders_april.csv")
both = pd.concat([march, april])
print(both.shape)
print(both[["order_id", "date", "product", "quantity"]])(12, 7)
order_id date product quantity
0 1001 2024-03-01 pen 12
1 1002 2024-03-01 notebook 5
2 1003 2024-03-02 bag 2
3 1004 2024-03-02 bottle 7
4 1005 2024-03-03 eraser 30
5 1006 2024-03-03 bag 1
6 1007 2024-03-04 pen 20
7 1008 2024-03-04 bottle 4
0 1009 2024-04-01 pen 15
1 1010 2024-04-01 notebook 3
2 1011 2024-04-02 stapler 6
3 1012 2024-04-02 bag 1Zwölf Zeilen, dieselben sieben Spalten. Pandas hat die Spalten nach Namen ausgerichtet, nicht nach Position — hätte die Datei für den April die Spalten in einer anderen Reihenfolge aufgelistet, wären sie dennoch unter den passenden Überschriften gelandet.
Aber schau dir den linken Rand an. Der Index läuft von 0 bis 7 und beginnt dann wieder bei 0. concat hat den jeweiligen Index jeder Tabelle beibehalten, sodass die Labels 0 bis 3 jetzt doppelt vorkommen. Das ist kein rein kosmetisches Problem:
print(both.loc[0, ["order_id", "date"]])order_id date
0 1001 2024-03-01
0 1009 2024-04-01Du hast nach Zeile 0 gefragt und zwei Zeilen erhalten. Jeder Code, der davon ausgeht, dass ein Label genau eine Zeile bedeutet — loc, ein späterer Merge über den Index, das Aktualisieren einer einzelnen Zelle —, tut nun etwas, das du nicht beabsichtigt hast. Du kannst das überprüfen:
print(both.index.is_unique)Falseignore_index=True — die Zeilen neu nummerieren
Wenn der alte Index nur eine simple Zeilennummerierung war (ein RangeIndex, wie bei jedem read_csv bisher), trägt er keinerlei Bedeutung in sich. Wirf ihn also weg und lass concat das Ergebnis bei null beginnend neu durchnummerieren:
orders = pd.concat([march, april], ignore_index=True)
print(orders.index.is_unique)
print(orders[["order_id", "date", "product"]].tail(5))True
order_id date product
7 1008 2024-03-04 bottle
8 1009 2024-04-01 pen
9 1010 2024-04-01 notebook
10 1011 2024-04-02 stapler
11 1012 2024-04-02 bagNun laufen die Labels von 0 bis 11, jedes genau einmal. Mache ignore_index=True zu deinem Standard beim Stapeln von Dateien; lasse es nur dann weg, wenn der Index eine echte Bedeutung hat (zum Beispiel nach set_index("order_id"), wo die Bestell-IDs bereits eindeutig sind).
Nachverfolgen, woher jede Zeile stammt
Nach dem Stapeln verrät in der Tabelle nichts mehr, zu welchem Monat eine Zeile gehört. Hier zeigt es das Datum, aber oft gibt es keinen solchen Hinweis. Füge vor dem Verketten eine Spalte hinzu — die Technik aus dem vorherigen Kapitel —, und die Information reist mit den Zeilen mit:
march["month"] = "March"
april["month"] = "April"
orders = pd.concat([march, april], ignore_index=True)
print(orders["month"].value_counts())month
March 8
April 4
Name: count, dtype: int64Die Anzahlen dienen gleichzeitig als Kontrolle: 8 + 4 = 12, nichts ist verloren gegangen.
Wenn die Spalten nicht übereinstimmen
Concat richtet Spalten nach ihrem Namen aus. Eine Spalte, die nur in manchen Tabellen existiert, erscheint dennoch im Ergebnis — und die Zeilen aus Tabellen, die diese Spalte nicht hatten, erhalten dort ein NaN. Angenommen, eine einzelne Bestellung wurde manuell erfasst, mit einer Spalte discount, aber ohne date, product, category oder price:
manual = pd.DataFrame({
"order_id": [2001],
"branch": ["north"],
"quantity": [3],
"discount": [0.1],
})
mixed = pd.concat([march.head(2), manual], ignore_index=True)
print(mixed[["order_id", "branch", "price", "discount"]])
print(mixed.isna().sum())order_id branch price discount
0 1001 north 15.0 NaN
1 1002 south 60.0 NaN
2 2001 north NaN 0.1
order_id 0
date 1
branch 0
product 1
category 1
quantity 0
price 1
month 1
discount 2
dtype: int64Kein Fehler. Das Ergebnis enthält die Vereinigungsmenge aller Spalten, und die Lücken werden mit NaN gefüllt. Genau deshalb verdient das Stapeln dasselbe isna().sum(), das du im Kapitel über fehlende Werte gelernt hast: Ein Spaltenname, der in einer Datei anders geschrieben wurde (Quantity statt quantity), schlägt nicht fehl, sondern erzeugt zwei halbleere Spalten. Wenn die Anzahl der Spalten nach einem Concat größer ist als in jeder einzelnen Eingabe, hat etwas nicht zusammengepasst.
axis=1 — und warum du das meistens nicht willst
pd.concat([a, b], axis=1) setzt Tabellen stattdessen nebeneinander und gleicht Zeilen anhand des Index-Labels ab. Das ist verlockend für die Aufgabe „die Produktspalten an die Bestellungen anhängen“, schaut sich den Produktnamen aber überhaupt nicht an: Zeile 0 der einen Tabelle landet neben Zeile 0 der anderen, völlig egal, was darin steht. Für den Abgleich über eine Spalte verwendest du merge.
Zeilen zusammenführen mit pd.merge
merge nimmt zwei Tabellen und einen Schlüssel entgegen. Für jede Zeile der linken Tabelle sucht es die Zeilen der rechten Tabelle mit demselben Schlüsselwert und fügt ihre Spalten zusammen:
products = pd.read_csv("products.csv")
merged = pd.merge(orders, products, on="product")
print(merged.shape)
print(merged[["order_id", "product", "quantity", "price", "cost", "supplier"]])(11, 10)
order_id product quantity price cost supplier
0 1001 pen 12 15.0 9.0 Alpha
1 1002 notebook 5 60.0 40.0 Alpha
2 1003 bag 2 850.0 600.0 Bravo
3 1004 bottle 7 120.0 80.0 Bravo
4 1005 eraser 30 8.0 5.0 Alpha
5 1006 bag 1 850.0 600.0 Bravo
6 1007 pen 20 15.0 9.0 Alpha
7 1008 bottle 4 120.0 80.0 Bravo
8 1009 pen 15 15.0 9.0 Alpha
9 1010 notebook 3 60.0 40.0 Alpha
10 1012 bag 1 850.0 600.0 BravoJede Bestellung hat nun ihre Kosten und ihren Lieferanten neben sich stehen — ganz ohne Schleife und ohne dass die Reihenfolge der Zeilen in einer der beiden Dateien eine Rolle spielt. Die Schlüsselspalte product taucht nur einmal auf, da sie auf beiden Seiten denselben Namen hat.
Vergleiche nun die Shape mit dem Plan. Wir haben 12 Zeilen vorhergesagt, falls alles behalten wird — und wir haben 11. Die Bestellung des Heftgeräts, 1011, ist verschwunden. Keine Warnung, kein Fehler.
Das ist kein Bug. Es ist das Standardverhalten des Merges, how="inner", das genau das tut, wofür es definiert ist: nur die Schlüssel behalten, die in beiden Tabellen vorkommen. Das Heftgerät steht nicht in products, also überlebt seine Bestellung nicht; der Textmarker steht nicht in orders, also taucht er ebenfalls nicht auf. Hättest du nicht 12 vorhergesagt, wäre dir der fehlende Verkauf niemals aufgefallen.
Die vier Arten von Merge
Das Argument how= entscheidet darüber, was mit Zeilen geschieht, deren Schlüssel auf der anderen Seite keinen Partner hat. Stelle dir die beiden Schlüsselmengen vor:
how="inner"(der Standard) behält nur Schlüssel, die in beiden Tabellen vorkommen. Nicht abgeglichene Zeilen auf beiden Seiten werden verworfen. Verwende dies, wenn du ausschließlich vollständige Datensätze benötigst.how="left"behält jede Zeile der linken Tabelle. Linke Zeilen ohne Partner erhaltenNaNin den Spalten der rechten Tabelle; nicht übereinstimmende Zeilen der rechten Tabelle werden verworfen. Verwende dies, um eine Haupttabelle anzureichern — die häufigste Wahl.how="right"ist das genaue Spiegelbild: Jede Zeile der rechten Tabelle bleibt erhalten, nicht übereinstimmende Zeilen der linken werden verworfen.how="outer"behält jeden Schlüssel von beiden Seiten und füllt die Lücken mitNaN. Verwende dies, um zwei Listen miteinander abzugleichen.
Die entscheidende Frage lautet: Welche Tabelle ist diejenige, deren Zeilen auf keinen Fall verschwinden dürfen? Hier sind es die Bestellungen. Jede Bestellung ist ein echter Verkauf; die Produktdatei dient nur zum Nachschlagen. Der richtige Merge ist also ein Left-Merge, mit den Bestellungen auf der linken Seite:
merged = pd.merge(orders, products, on="product", how="left")
print(merged.shape)
print(merged[["order_id", "product", "cost", "supplier"]])(12, 10)
order_id product cost supplier
0 1001 pen 9.0 Alpha
1 1002 notebook 40.0 Alpha
2 1003 bag 600.0 Bravo
3 1004 bottle 80.0 Bravo
4 1005 eraser 5.0 Alpha
5 1006 bag 600.0 Bravo
6 1007 pen 9.0 Alpha
7 1008 bottle 80.0 Bravo
8 1009 pen 9.0 Alpha
9 1010 notebook 40.0 Alpha
10 1011 stapler NaN NaN
11 1012 bag 600.0 BravoZwölf Zeilen, genau wie vorhergesagt. Die Bestellung des Heftgeräts ist immer noch da, mit NaN bei den Kosten und beim Lieferanten — was ehrlich ist: Die Kosten sind unbekannt, und genau das ist hier sichtbar. Ein fehlender Wert, den du siehst, ist weitaus besser als eine fehlende Zeile, die du nicht bemerkst. Ab hier greifen die Werkzeuge aus dem Kapitel über fehlende Werte: Du kannst die Lücken mit isna().sum() zählen, die Kosten eintragen, sobald der Einkauf sie dir mitteilt, oder die Bestellung separat melden.
Ein Right-Merge ist das Spiegelbild — jedes Produkt bleibt erhalten, der Textmarker ohne zugehörige Bestellung:
right = pd.merge(orders, products, on="product", how="right")
print(right.shape)
print(right[["order_id", "product", "cost"]].tail(3))(12, 10)
order_id product cost
9 1008.0 bottle 80.0
10 1005.0 eraser 5.0
11 NaN marker 25.0Beachte, wie sich order_id von ganzen Zahlen zu 1008.0, 1005.0 usw. verändert hat. Die Zeile des Textmarkers besitzt keine Bestell-ID, daher enthält die Spalte nun ein NaN — und wie du im Kapitel über fehlende Werte gesehen hast, wird eine Ganzzahlspalte, die ein NaN empfängt, zu float64. Wenn IDs nach einem Merge plötzlich ein .0 bekommen, ist genau das der Grund: Manche Zeilen haben keinen Partner gefunden.
indicator=True — sehen, woher jede Zeile stammt
Ein Outer-Merge behält alles von beiden Seiten. Fügst du indicator=True hinzu, ergänzt pandas eine Spalte _merge, die für jede Zeile angibt, ob ihr Schlüssel in beiden Tabellen, nur in der linken oder nur in der rechten gefunden wurde:
audit = pd.merge(orders, products, on="product", how="outer", indicator=True)
print(audit[["order_id", "product", "cost", "_merge"]])
print(audit["_merge"].value_counts())order_id product cost _merge
0 1003.0 bag 600.0 both
1 1006.0 bag 600.0 both
2 1012.0 bag 600.0 both
3 1004.0 bottle 80.0 both
4 1008.0 bottle 80.0 both
5 1005.0 eraser 5.0 both
6 NaN marker 25.0 right_only
7 1002.0 notebook 40.0 both
8 1010.0 notebook 40.0 both
9 1001.0 pen 9.0 both
10 1007.0 pen 9.0 both
11 1009.0 pen 9.0 both
12 1011.0 stapler NaN left_only
_merge
both 11
left_only 1
right_only 1
Name: count, dtype: int64Das ist der Prüfbericht des Merges selbst. Elf Zeilen stimmten überein; eine Bestellung hat keine Produktzeile (left_only: das Heftgerät); ein Produkt hat keine Bestellung (right_only: der Textmarker). Es sind dieselben Informationen wie aus Schritt 5 des Plans, diesmal direkt vom Merge selbst geliefert.
Der Outer-Merge hat die Zeilen zudem nach dem Schlüssel sortiert — bag, bottle, eraser, … —, anstatt die Reihenfolge der Bestellungen beizubehalten. Inner- und Left-Merges behalten die Reihenfolge der linken Tabelle bei; ein Outer-Merge garantiert das nicht. Spielt die Reihenfolge eine Rolle, sortiere im Anschluss, wie in Kapitel sieben beschrieben.
Die Spalte _merge enthält ganz normale Daten, du kannst also danach filtern. Nur die Problemfälle aufzulisten ist ein Einzeiler:
problems = audit[audit["_merge"] != "both"]
print(problems[["order_id", "product", "_merge"]])order_id product _merge
6 NaN marker right_only
12 1011.0 stapler left_onlyFühre eine solche Prüfung immer durch, wenn du Daten zusammenführst, die du nicht selbst erstellt hast. Wenn die Liste der Probleme leer ist, hast du den Beweis, dass beide Dateien zusammenpassen.
Die stille Gefahr: doppelte Schlüssel
Bisher enthielt products genau eine Zeile pro Produkt. Nehmen wir an, der Einkauf nimmt einen zweiten Stift-Lieferanten mit etwas höheren Kosten auf und hängt einfach eine Zeile an, sodass pen nun doppelt vorkommt:
products_dup = pd.concat(
[products, pd.DataFrame({"product": ["pen"], "cost": [10.0], "supplier": ["Gamma"]})],
ignore_index=True,
)
print(products_dup["product"].is_unique)
exploded = pd.merge(march, products_dup, on="product", how="left")
print(march.shape, "->", exploded.shape)
print(exploded[["order_id", "product", "quantity", "cost", "supplier"]])False
(8, 8) -> (10, 10)
order_id product quantity cost supplier
0 1001 pen 12 9.0 Alpha
1 1001 pen 12 10.0 Gamma
2 1002 notebook 5 40.0 Alpha
3 1003 bag 2 600.0 Bravo
4 1004 bottle 7 80.0 Bravo
5 1005 eraser 30 5.0 Alpha
6 1006 bag 1 600.0 Bravo
7 1007 pen 20 9.0 Alpha
8 1007 pen 20 10.0 Gamma
9 1008 bottle 4 80.0 BravoMärz hatte 8 Bestellungen; der Merge hat 10 Zeilen zurückgegeben. Die Bestellungen 1001 und 1007 — die beiden Bestellungen für Stifte — tauchen jeweils zweimal auf, einmal pro pen-Zeile. Nichts ist fehlgeschlagen. Summierst du jetzt jedoch die Bestellmenge, werden die Stifte doppelt gezählt: 32 zusätzliche Stifte, die nie verkauft wurden.
print(march["quantity"].sum(), exploded["quantity"].sum())81 11381 gegenüber 113. Das nennt man Zeilenexplosion (row explosion), und es ist der teuerste aller Merge-Fehler, weil das Ergebnis vollkommen normal aussieht. Die dahinterliegende Regel lautet: Ein Merge verknüpft jede passende linke Zeile mit jeder passenden rechten Zeile. Eine 1-zu-1- oder n-zu-1-Beziehung behält die Zeilenanzahl bei; ein Schlüssel, der sich auf beiden Seiten wiederholt, vervielfacht sie.
Ein Merge, der neue Zeilen hinzufügt, weist niemals darauf hin. Das einzige Symptom ist eine Gesamtsumme, die zu hoch ausfällt — und eine zu hohe Summe wirkt selten verdächtig. Vergleiche vor und nach jedem Merge len(); das ist die eine Prüfung, die dieses Problem zuverlässig aufdeckt.validate= — pandas die Prüfung überlassen
Du kannst die erwartete Beziehung explizit angeben, und pandas wird den Merge verweigern, falls die Daten dagegen verstoßen. Viele Bestellungen teilen sich ein einziges Produkt, also sollte dieser Merge many-to-one (n-zu-1) sein:
try:
pd.merge(march, products_dup, on="product", how="left", validate="many_to_one")
except Exception as error:
print(type(error).__name__ + ":", error)MergeError: Merge keys are not unique in right dataset; not a many-to-one merge
Duplicates in right:
product
pen ...Ein MergeError, und er listet sogar den problematischen Schlüssel auf. Die weiteren Werte sind "one_to_one", "one_to_many" und "many_to_many". Das Angeben von validate= kostet nichts und verwandelt ein stilles, falsches Ergebnis in einen unüberhörbaren Fehler. Setze es daher bei jedem Merge mit einer Lookup-Tabelle ein.
Das Problem beheben: entscheiden, welches Duplikat das richtige ist
Der Fehler sagt dir, dass etwas nicht stimmt; zu entscheiden, was richtig ist, ist deine Aufgabe. Sieh dir zuerst die Duplikate an:
print(products_dup[products_dup["product"].duplicated(keep=False)])product cost supplier
0 pen 9.0 Alpha
6 pen 10.0 Gammaduplicated(keep=False) markiert jedes Exemplar, nicht nur das zweite. Jetzt ist eine fachliche Entscheidung gefragt. Wenn die neueste Zeile die alte ersetzt, behalte das letzte Exemplar:
products_clean = products_dup.drop_duplicates(subset="product", keep="last")
fixed = pd.merge(march, products_clean, on="product", how="left", validate="many_to_one")
print(fixed.shape)
print(fixed.loc[fixed["product"] == "pen", ["order_id", "cost", "supplier"]])(8, 10)
order_id cost supplier
0 1001 10.0 Gamma
6 1007 10.0 GammaWieder 8 Zeilen, eine pro Bestellung. Wenn stattdessen tatsächlich beide Lieferanten Stifte verkaufen und in der Bestellung festgehalten wird, welcher gemeint ist, dann gehört supplier ebenfalls in die Bestellungen — und der Merge-Schlüssel besteht aus zwei Spalten: on=["product", "supplier"]. Der Code kann diese Entscheidung nicht für dich treffen; deine Planung sehr wohl.
Wenn die Schlüsselspalten unterschiedliche Namen haben
In branches.csv heißt die Spalte branch_name, während die Bestellungen sie branch nennen. on= setzt auf beiden Seiten denselben Namen voraus; benenne daher jede Seite separat mit left_on= und right_on=:
branches = pd.read_csv("branches.csv")
with_manager = pd.merge(orders, branches, left_on="branch", right_on="branch_name", how="left")
print(list(with_manager.columns))['order_id', 'date', 'branch', 'product', 'category', 'quantity', 'price', 'month', 'branch_name', 'manager']Beide Schlüsselspalten bleiben erhalten, da pandas nicht wissen kann, dass du sie als eine einzige Spalte betrachtest. branch und branch_name enthalten jetzt identische Werte, also entferne eine davon:
with_manager = with_manager.drop(columns="branch_name")
print(with_manager[["order_id", "branch", "manager"]].head(4))order_id branch manager
0 1001 north Mira
1 1002 south Omar
2 1003 north Mira
3 1004 east LenaDie Alternative besteht darin, die Spalte vor dem Zusammenführen umzubenennen — branches.rename(columns={"branch_name": "branch"}) — und danach das einfache on="branch" zu nutzen. Beides ist völlig in Ordnung; das vorherige Umbenennen hält das Ergebnis direkt sauber, ohne dass ein nachgelagerter Bereinigungsschritt nötig ist.
Wenn beide Tabellen eine Spalte mit demselben Namen haben
Existiert eine Spalte, die nicht der Schlüssel ist, in beiden Tabellen, kann pandas nicht zwei Spalten namens price in einer Tabelle unterbringen. Daher versieht es sie mit den Suffixen _x (links) und _y (rechts). Stelle dir eine Tabelle mit Listenpreisen vor, die ihre Spalte ebenfalls price nennt:
list_prices = pd.DataFrame({"product": ["pen", "bag"], "price": [14.0, 800.0]})
compared = pd.merge(march, list_prices, on="product")
print(compared[["order_id", "product", "price_x", "price_y"]])order_id product price_x price_y
0 1001 pen 15.0 14.0
1 1003 bag 850.0 800.0
2 1006 bag 850.0 800.0
3 1007 pen 15.0 14.0price_x und price_y funktionieren zwar, aber in drei Wochen erinnert sich niemand mehr daran, welche davon welche ist. Vergib die Namen daher selbst mit suffixes=:
compared = pd.merge(march, list_prices, on="product", suffixes=("_sold", "_list"))
print(compared[["order_id", "price_sold", "price_list"]])order_id price_sold price_list
0 1001 15.0 14.0
1 1003 850.0 800.0
2 1006 850.0 800.0
3 1007 15.0 14.0Jetzt sagen die Spalten genau aus, was sie bedeuten, und der bei jeder Bestellung gewährte Rabatt ist einfach price_list - price_sold. (Code, der später nach compared["price"] fragt, würde einen KeyError auslösen, da nach dem Merge keine Spalte mehr diesen exakten Namen trägt — ein weiterer Grund, die Namen ganz bewusst zu wählen.)
join — kurz über das Zusammenführen nach Index
DataFrames verfügen außerdem über eine Methode .join(). Sie führt einen Merge durch, der den Index der rechten Tabelle als Schlüssel verwendet — praktisch, wenn die Lookup-Tabelle ohnehin bereits über den Schlüssel indiziert ist:
by_product = products.set_index("product")
joined = orders.join(by_product, on="product")
print(joined[["order_id", "product", "cost", "supplier"]].head(3))order_id product cost supplier
0 1001 pen 9.0 Alpha
1 1002 notebook 40.0 Alpha
2 1003 bag 600.0 Bravojoin verwendet standardmäßig how="left". Es erfüllt dieselbe Aufgabe wie pd.merge(..., how="left"); merge ist das allgemeinere Werkzeug und dasjenige, das wir im weiteren Verlauf dieses Kurses verwenden.
Ein vollständiges Beispiel
Zurück zur Frage der Ladenbesitzerin: Gewinn pro Lieferant über März und April hinweg, wobei jede einzelne Bestellung berücksichtigt wird.
supplier_profit.py:
import pandas as pd
march = pd.read_csv("orders.csv")
april = pd.read_csv("orders_april.csv")
products = pd.read_csv("products.csv")
# 1. Stack the two months, remembering where each row came from.
march["month"] = "March"
april["month"] = "April"
orders = pd.concat([march, april], ignore_index=True)
print("orders:", orders.shape)
# 2. The lookup key must be unique, or the merge will duplicate orders.
assert products["product"].is_unique
# 3. Attach cost and supplier to every order. Keep every order.
sales = pd.merge(
orders, products, on="product", how="left",
validate="many_to_one", indicator=True,
)
print("after merge:", sales.shape)
assert len(sales) == len(orders)
# 4. Report the orders that found no product row, then set them aside.
unmatched = sales[sales["_merge"] == "left_only"]
print("no cost on file:", unmatched["order_id"].tolist(), unmatched["product"].tolist())
sales = sales[sales["_merge"] == "both"].drop(columns="_merge")
# 5. Profit per row, then per supplier and month.
sales["profit"] = sales["quantity"] * (sales["price"] - sales["cost"])
report = (
sales.groupby(["supplier", "month"], as_index=False)["profit"]
.sum()
.sort_values(["supplier", "profit"], ascending=[True, False])
.reset_index(drop=True)
)
print()
print(report)
print()
print(sales.groupby("supplier")["profit"].sum().sort_values(ascending=False))orders: (12, 8)
after merge: (12, 11)
no cost on file: [1011] ['stapler']
supplier month profit
0 Alpha March 382.0
1 Alpha April 150.0
2 Bravo March 1190.0
3 Bravo April 250.0
supplier
Bravo 1440.0
Alpha 532.0
Name: profit, dtype: float64Warum es so geschrieben ist:
- Jedem Kombinationsschritt folgt eine Shape.
orders: (12, 8)bestätigt das Stapeln (8 + 4 Zeilen, 7 Spalten +month).after merge: (12, 11)bestätigt, dass nichts verworfen oder dupliziert wurde — 3 neue Spalten:cost,supplier,_merge. Die Shapes sind der schnellste und einfachste Beweis dafür, dass jeder Schritt genau das getan hat, was der Plan vorsah. - Die Annahmen sind als Code festgehalten.
assert products["product"].is_uniqueundvalidate="many_to_one"schützen beide vor einer Zeilenexplosion;assert len(sales) == len(orders)sichert die Vorhersage aus dem Plan ab. Wenn im nächsten Monat jemand eine Produktdatei mit einem Duplikat schickt, stoppt das Skript, anstatt einen künstlich aufgeblähten Gewinn auszugeben. how="left"behält die Heftgerät-Bestellung undindicator=Truebenennt sie. Das Skript verwirft sie nicht still und heimlich, sondern gibt[1011] ['stapler']aus, sodass die Ladenbesitzerin weiß, dass in der April-Summe ein Verkauf fehlt, bis der Einkauf einen Einkaufspreis hinterlegt. Diese Zeile ist Teil der Antwort, keine bloße Debug-Ausgabe.- Die Spalte
monthwurde vor demconcathinzugefügt. Andernfalls müsste für die monatliche Aufteilung im Bericht das Datum geparst und umgewandelt werden — machbar, aber mehr Arbeit für dasselbe Ergebnis. - Der Gewinn wird erst nach dem Merge berechnet, weil dafür Spalten aus beiden Dateien benötigt werden. Das ist die natürliche Reihenfolge: kombinieren, dann Spalten hinzufügen (Kapitel neun), dann gruppieren (Kapitel acht), dann sortieren (Kapitel sieben).
Die Antwort: Bravo brachte 1.440 Gewinn ein und Alpha 532, wobei ein April-Verkauf noch auf seine Kosten wartet. Bravo verkauft zwar wenige Artikel, aber Taschen und Flaschen bringen eine weitaus höhere Marge pro Stück ein als Stifte und Radiergummis.
Wenn etwas schiefgeht
ValueError: You are trying to merge on int64 and str columns for key 'order_id'. If you wish to proceed you should use pd.concat
Der Schlüssel hat auf jeder Seite einen anderen Datentyp. Hier ist eine Lieferdatei, deren Bestell-IDs mit einem vorangestellten # exportiert wurden. Speichere sie als deliveries.csv — sie wird auch in „Jetzt bist du dran“ verwendet. (Falls du das gleichnamige Fahrerprotokoll aus dem Kapitel über fehlende Werte noch hast, überschreibe es; dies ist eine andere Datei.)
order_id,status
#1001,delivered
#1002,delivered
#1003,returned
#1005,delivered
#1006,pendingimport pandas as pd
march = pd.read_csv("orders.csv")
deliveries = pd.read_csv("deliveries.csv")
print(march["order_id"].dtype, deliveries["order_id"].dtype)
try:
pd.merge(march, deliveries, on="order_id", how="left")
except ValueError as error:
print("ValueError:", error)int64 str
ValueError: You are trying to merge on int64 and str columns for key 'order_id'. If you wish to proceed you should use pd.concat1001 und "#1001" sind unterschiedliche Werte, und pandas weigert sich zu raten. (Der Hinweis auf pd.concat gilt für eine andere Situation; ignoriere ihn.) Die Lösung besteht darin, den Schlüsseln denselben Typ und denselben Inhalt zu geben — entferne das # und konvertiere anschließend:
deliveries["order_id"] = deliveries["order_id"].str.removeprefix("#").astype("int64")
status = pd.merge(march, deliveries, on="order_id", how="left")
print(status[["order_id", "product", "status"]])order_id product status
0 1001 pen delivered
1 1002 notebook delivered
2 1003 bag returned
3 1004 bottle NaN
4 1005 eraser delivered
5 1006 bag pending
6 1007 pen NaN
7 1008 bottle NaNDie Bestellungen ohne Liefereintrag erhalten ein NaN für status — der Left-Merge behält sie bei, und genau das ist gewünscht, wenn die Frage lautet: „Welche Bestellungen wurden noch nicht geliefert?“
MergeError: Merge keys are not unique in right dataset; not a many-to-one merge Du hast validate="many_to_one" verwendet, und die rechte Tabelle enthält einen doppelten Schlüssel. Die Fehlermeldung listet die Duplikate auf. Entferne validate nicht, nur um die Fehlermeldung loszuwerden — sieh dir die doppelten Zeilen mit duplicated(keep=False) an und entscheide, welche davon die richtige ist, genau wie im Abschnitt über Zeilenexplosion beschrieben.
KeyError: 'branch' Die in on= angegebene Spalte fehlt in einer der beiden Tabellen:
branches = pd.read_csv("branches.csv")
try:
pd.merge(march, branches, on="branch")
except KeyError as error:
print("KeyError:", error)KeyError: 'branch'In branches heißt die Spalte branch_name. Gib list(df.columns) für beide Tabellen aus; verwende left_on="branch", right_on="branch_name" oder benenne eine Seite um. Ein nachgestelltes Leerzeichen im Spaltennamen ("branch ") führt zum selben Fehler und fällt nur in der Listenform auf.
Der Merge lief durch, aber die neuen Spalten enthalten ausschließlich NaN Kein Fehler, aber jede Zeile blieb ohne Übereinstimmung. Die Schlüssel sehen gleich aus, sind es aber nicht: "north " mit einem nachgestellten Leerzeichen, "South" mit einem Großbuchstaben oder dieselbe ID in einer Datei als Text und in der anderen mit einem Präfix. Prüfe das vor dem Merge mit isin:
branches_messy = pd.DataFrame({"branch_name": ["north ", "South", "east"], "manager": ["Mira", "Omar", "Lena"]})
print(march["branch"].isin(branches_messy["branch_name"]).sum(), "of", len(march), "orders match")
branches_messy["branch_name"] = branches_messy["branch_name"].str.strip().str.lower()
print(march["branch"].isin(branches_messy["branch_name"]).sum(), "of", len(march), "orders match")2 of 8 orders match
8 of 8 orders matchZwei von acht vor der Bereinigung, acht von acht danach. Bereinige den Schlüssel auf beiden Seiten auf dieselbe Weise — str.strip() und einheitliche Groß-/Kleinschreibung —, bevor du zusammenführst.
Die Zeilenanzahl ist nach dem Merge gestiegen Die rechte Tabelle enthält einen doppelten Schlüssel; siehe den Abschnitt „Die stille Gefahr“ weiter oben. Vergleiche len() vor und nach jedem Merge und verwende validate=.
Zeilen sind nach dem Merge verschwunden Du hast den Standard how="inner" verwendet und manche Schlüssel hatten keinen Partner. Verwende how="left" mit der Haupttabelle auf der linken Seite und indicator=True, um zu sehen, welche Zeilen ohne Übereinstimmung geblieben sind.
TypeError: concat() takes 1 positional argument but 2 were given
try:
pd.concat(march, deliveries)
except TypeError as error:
print("TypeError:", error)TypeError: concat() takes 1 positional argument but 2 were givenconcat nimmt ein Argument entgegen: eine Liste von Tabellen. Schreibe pd.concat([march, april]) — auf die eckigen Klammern kommt es an.
Ein duplizierter Index nach concat, und loc gibt zwei Zeilen zurück Jede Eingabe hat ihr eigenes 0, 1, 2, … behalten. Füge ignore_index=True hinzu.
Schritt 4 von 6 — Vorhersagen
Prüfe dein Verständnis
März hat 8 Bestellungen und April 4. Was wird ausgegeben?
from io import StringIO
import pandas as pd
# Stand in for orders.csv, orders_april.csv and products.csv,
# so this snippet runs on its own.
MARCH = """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
"""
APRIL = """order_id,date,branch,product,category,quantity,price
1009,2024-04-01,north,pen,stationery,15,15.0
1010,2024-04-01,east,notebook,stationery,3,60.0
1011,2024-04-02,south,stapler,stationery,6,95.0
1012,2024-04-02,north,bag,accessories,1,850.0
"""
PRODUCTS = """product,cost,supplier
pen,9.0,Alpha
notebook,40.0,Alpha
bag,600.0,Bravo
bottle,80.0,Bravo
eraser,5.0,Alpha
marker,25.0,Alpha
"""
march = pd.read_csv(StringIO(MARCH))
april = pd.read_csv(StringIO(APRIL))
products = pd.read_csv(StringIO(PRODUCTS))
both = pd.concat([march, april])
print(both.shape, both.index[-1])- A(12, 7) 11
- B(12, 14) 3
- C(12, 7) 3
- D(8, 7) 7
Jemand hat der Produktliste eine zweite Zeile für pen hinzugefügt. Was wird ausgegeben?
from io import StringIO
import pandas as pd
# Stand in for orders.csv, orders_april.csv and products.csv,
# so this snippet runs on its own.
MARCH = """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
"""
APRIL = """order_id,date,branch,product,category,quantity,price
1009,2024-04-01,north,pen,stationery,15,15.0
1010,2024-04-01,east,notebook,stationery,3,60.0
1011,2024-04-02,south,stapler,stationery,6,95.0
1012,2024-04-02,north,bag,accessories,1,850.0
"""
PRODUCTS = """product,cost,supplier
pen,9.0,Alpha
notebook,40.0,Alpha
bag,600.0,Bravo
bottle,80.0,Bravo
eraser,5.0,Alpha
marker,25.0,Alpha
"""
march = pd.read_csv(StringIO(MARCH))
april = pd.read_csv(StringIO(APRIL))
products = pd.read_csv(StringIO(PRODUCTS))
products = pd.concat([products, pd.DataFrame({"product": ["pen"], "cost": [10.0], "supplier": ["Charlie"]})])
report = pd.merge(march, products, on="product", how="left")
print(len(march), len(report))- A8 6
- B8 8
- CEin `MergeError` wegen doppelter Schlüssel
- D8 10 — jede pen-Bestellung stimmte mit beiden pen-Zeilen überein und wurde verdoppelt, ganz ohne Fehler
Jede März-Bestellung benötigt die Kosten ihres Produkts, und keine Bestellung darf verloren gehen, selbst wenn ihr Produkt in der Liste fehlt. Welches how= verwendest du?
- A`how="outer"`
- B`how="right"`
- C`how="inner"`
- D`how="left"`, mit den Bestellungen als linke Tabelle
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 Lieferteam hat deliveries.csv geschickt (gezeigt in „Wenn etwas schiefgeht“) — eine Zeile pro Bestellung, für die dort ein Eintrag vorliegt, mit Bestell-IDs in der Form #1001. Die Ladenbesitzerin wünscht sich einen Lieferbericht über März und April zusammen.
Schreibe delivery_report.py so, dass:
- Jede Bestellung beider Monate erhalten bleibt — genau eine Zeile pro Bestellung, überprüft mit einem
assertauf die Zeilenanzahl, der Merge mitvalidate=abgesichert und mitindicator=Truegeprüft. - Die Schlüssel wirklich übereinstimmen — der Lieferschlüssel wird auf denselben Typ wie
order_idbereinigt, und das Skript stoppt, falls eine Bestellung doppelt in der Lieferdatei vorkommt. - Fehlende Werte „no record“ bedeuten — Bestellungen ohne Lieferzeile erhalten den Status
"no record", niemals ein stillschweigendesNaN. - Folgendes in dieser Reihenfolge ausgegeben wird: die gestapelte Shape, die zusammengeführte Shape, die Anzahlen von
_merge, die Anzahl der Bestellungen pro Status, die Bestellungen ohne Eintrag (order_id,branch,product, sortiert nachorder_id) und die Anzahl der gelieferten Bestellungen pro Filiale.
Wenn alles stimmt, ist die Ausgabe exakt:
orders: (12, 7)
after merge: (12, 9)
_merge
left_only 7
both 5
right_only 0
Name: count, dtype: int64
status
no record 7
delivered 3
returned 1
pending 1
Name: count, dtype: int64
order_id branch product
3 1004 east bottle
6 1007 east pen
7 1008 north bottle
8 1009 north pen
9 1010 east notebook
10 1011 south stapler
11 1012 north bag
branch
north 2
south 1
dtype: int64Bevor du irgendeinen Code schreibst, erkläre jede Zahl in dieser Ausgabe allein anhand der Dateien: Warum 12 Zeilen nach dem Stapeln, warum weiterhin 12 nach dem Merge und warum 7 Bestellungen ohne Eintrag. Wenn du das nicht kannst, ist der Plan noch nicht fertig.
Bringe es dann zweimal absichtlich zum Scheitern:
- Entferne
.str.removeprefix("#"). Welchen Fehler erhältst du und welche Zeile löst ihn aus? - Füge eine zweite Zeile für
#1003zudeliveries.csvhinzu (etwa#1003,delivered). Was bewirktvalidate=jetzt? Was hätte der Bericht ohne dieses Argument ausgegeben?
Lösung
Der Plan. Die Fragestellung: „Wie ist der Lieferstatus für jede Bestellung im März und April?“ — das Ergebnis hat also eine Zeile pro Bestellung, 12 Zeilen. März und April haben identische Spalten: Stapele sie mit concat und ignore_index=True (8 + 4 = 12). Die Lieferdatei ist nach Bestell-ID indiziert, allerdings als Text mit einem #, daher muss der Schlüssel vor dem Abgleich mit dem ganzzahligen order_id bereinigt werden. Jede Bestellung sollte maximal einmal darin vorkommen, die Beziehung ist also one-to-one (1-zu-1: jede Bestellung hat maximal eine Lieferzeile, jede Lieferzeile gehört zu einer Bestellung). Jede Bestellung muss erhalten bleiben, daher gehören die Bestellungen nach links und der Merge lautet how="left". Es gibt fünf Lieferzeilen, alle für den März, also sollten 12 − 5 = 7 Bestellungen am Ende ohne Eintrag dastehen.
delivery_report.py:
import pandas as pd
march = pd.read_csv("orders.csv")
april = pd.read_csv("orders_april.csv")
deliveries = pd.read_csv("deliveries.csv")
# 1. Stack the months.
orders = pd.concat([march, april], ignore_index=True)
print("orders:", orders.shape)
# 2. Clean the key, then check it.
deliveries["order_id"] = deliveries["order_id"].str.removeprefix("#").astype("int64")
assert deliveries["order_id"].is_unique, "an order appears twice in deliveries.csv"
# 3. Keep every order.
report = pd.merge(
orders, deliveries, on="order_id", how="left",
validate="one_to_one", indicator=True,
)
print("after merge:", report.shape)
assert len(report) == len(orders)
print(report["_merge"].value_counts())
# 4. Missing status means the delivery team has no record.
report["status"] = report["status"].fillna("no record")
# 5. Orders per status.
print()
print(report["status"].value_counts())
# 6. Orders with no record.
missing = report.loc[report["status"] == "no record", ["order_id", "branch", "product"]]
print()
print(missing.sort_values("order_id"))
# 7. Delivered orders per branch.
delivered = report[report["status"] == "delivered"]
print()
print(delivered.groupby("branch").size())orders: (12, 7)
after merge: (12, 9)
_merge
left_only 7
both 5
right_only 0
Name: count, dtype: int64
status
no record 7
delivered 3
returned 1
pending 1
Name: count, dtype: int64
order_id branch product
3 1004 east bottle
6 1007 east pen
7 1008 north bottle
8 1009 north pen
9 1010 east notebook
10 1011 south stapler
11 1012 north bag
branch
north 2
south 1
dtype: int64Jede Vorhersage trifft zu: 12 Zeilen nach dem Stapeln, 12 nach dem Merge, 7 ohne Eintrag. Die Zählwerte von _merge bestätigen das unabhängig: 5 both, 7 left_only, 0 right_only (ein Left-Merge erzeugt niemals right_only).
Hinweise zu den Entscheidungen:
validate="one_to_one", nicht"many_to_one". Die Tabelle der Bestellungen hat ebenfalls eindeutige Bestell-IDs, beide Seiten sind also eindeutig. Die strengere Prüfung würde auch eine doppelte Bestell-ID bei den Bestellungen abfangen — zum Beispiel, wenn die April-Datei versehentlich eine März-Bestellung wiederholt hätte.- Das
assertaufis_uniquesteht vor dem Merge, versehen mit einer Nachricht, sodass der Fehler in einfachen Worten mitteilt, was nicht stimmt, statt in Merge-Begriffen. fillna("no record")wird erst nach dem Merge angewendet, niemals davor: Die fehlenden Status existieren erst, nachdem die Bestellungen ohne Lieferzeile durch den Left-Merge übernommen wurden.- Die Filiale east hat keine zugestellten Bestellungen, daher taucht sie in der letzten Tabelle gar nicht auf. Gruppieren erzeugt nur Gruppen, die tatsächlich in den Daten vorkommen — benötigt die Ladenbesitzerin eine 0 für east, wäre das ein Merge der Filialliste mit diesem Ergebnis,
how="left", gefolgt von einemfillna(0).
Die beiden Fehlerfälle. Ohne removeprefix schlägt die Zeile mit astype("int64") zuerst fehl mit ValueError: invalid literal for int() with base 10: '#1001', weil sich "#1001" nicht in eine Zahl umwandeln lässt — der Fehler weist direkt auf die Bereinigungszeile hin, wo auch die Lösung hingehört. Lässt du zusätzlich das astype weg, löst der Merge selbst ValueError: You are trying to merge on int64 and str columns for key 'order_id' aus. Mit einer zweiten #1003-Zeile stoppt das assert das Skript mit AssertionError: an order appears twice in deliveries.csv; entfernst du das Assert, löst validate="one_to_one" die Exception MergeError: Merge keys are not unique in right dataset; not a one-to-one merge aus. Ohne diese Schutzmechanismen hätte der Bericht 13 Zeilen für 12 Bestellungen angezeigt und Bestellung 1003 doppelt gezählt — einmal returned, einmal delivered — und jede Gesamtsumme darunter wäre stillschweigend falsch gewesen.
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