pandas

pandas
Python
R
Data Wrangling
Series und DataFrame, Auswahl, Aggregation und Zusammenführen.

Kernideen

  • Eine Series ist ein Vektor mit Index, ein DataFrame eine Sammlung solcher Spalten mit gemeinsamem Zeilenindex.
  • Rechnungen zwischen Series werden nach Index ausgerichtet, nicht nach Position. Benannte Vektoren in R rechnen nach Position.
  • .loc wählt nach Beschriftung (Grenze eingeschlossen), .iloc nach Position (Grenze ausgeschlossen). Bedingungen brauchen runde Klammern.
  • Zeilen- und Spaltenauswahl gehören in einen .loc-Zugriff; Kettenindizierung schreibt ins Leere.
  • Aggregationen lassen NaN still weg, und groupby lässt Zeilen mit fehlendem Gruppenschlüssel ebenso still fallen.
  • Nach jedem merge die Zeilenzahl prüfen: Ein nicht eindeutiger Schlüssel vervielfacht Zeilen und verfälscht jede Summe.

Erklärung

Vorwissen: NumPy und SciPy, denn pandas legt eine beschriftete Ebene über NumPy-Arrays, und R-Grundlagen für den Data Frame als Vergleich. Die Arbeitsschritte selbst, also filtern, umformen und zusammenfassen, stehen mit dplyr und pandas nebeneinander unter Data Wrangling.

Was pandas über NumPy legt

Ein NumPy-Array kennt nur Positionen. Für Daten braucht man Namen: Spalten heissen verbrauch und nicht 2, Zeilen gehören zu einem Datum und nicht zu Position 17. Genau das ergänzt pandas, und zwar über den Index.

Der Index ist mehr als eine Beschriftung. Bei jeder Rechnung zwischen zwei Series oder DataFrames sucht pandas zuerst die passenden Einträge nach Index und rechnet dann. Fehlt ein Eintrag auf einer Seite, entsteht NaN. Das ist der grösste konzeptionelle Unterschied zum R-Data-Frame, der keinen vergleichbaren Mechanismus hat; dort gehen Rechnungen immer nach Position. Wo keine Ausrichtung erwünscht ist, hilft reset_index(drop=True) oder das Rechnen auf .to_numpy().

Die Datenlage dieser Seite

24 wöchentliche Verbrauchsmessungen vom 5. Januar bis 23. März 2026, je eine für die Standorte Bern und Chur. Die Werte sind fest eingetragen, damit R und Python dieselben Zahlen zeigen.

Auswahl

Zugriff arbeitet mit obere Grenze Beispiel
df["a"] Spaltenname eine Spalte als Series
df[["a", "b"]] Liste von Spaltennamen mehrere Spalten als DataFrame
.loc[zeilen, spalten] Beschriftungen und Bedingungen eingeschlossen df.loc["b":"c", "wert"]
.iloc[zeilen, spalten] Positionen ausgeschlossen df.iloc[1:3, 0]
.query("...") Bedingung als Text df.query("wert > 3")

Bedingungen werden mit & (und), | (oder) und ~ (nicht) verknüpft, nicht mit and und or, und jede Teilbedingung braucht runde Klammern. Der Grund ist die Rangfolge: In Python bindet & stärker als == und >. In R ist es umgekehrt, dort geht es ohne Klammern.

Warum Kettenindizierung nicht funktioniert

df[bedingung]["wert"] = 99 besteht aus zwei Zugriffen. Der erste erzeugt ein Zwischenergebnis, der zweite schreibt in dieses Zwischenergebnis, das danach verworfen wird. Bei einer Auswahl per Bedingung ist das Zwischenergebnis eine Kopie, die Zuweisung bewirkt nichts, und pandas warnt: bis Version 2 mit SettingWithCopyWarning, ab Version 3 (copy on write, dort gilt es für jedes Zwischenergebnis) mit ChainedAssignmentError. Richtig ist ein einziger Zugriff: df.loc[bedingung, "wert"] = 99. In R ist die verkettete Form df$wert[bedingung] <- 99 dagegen gültig und üblich.

Fehlende Werte

pandas lässt NaN in Aggregationen weg (skipna=True), R gibt NA zurück und zwingt mit na.rm = TRUE zur Entscheidung. Wer in Python denselben Reflex will, zählt vor jeder Aggregation mit isna().sum(). Dazu kommt eine zweite, weniger bekannte Stelle: groupby wirft Zeilen, deren Gruppenschlüssel fehlt, ohne Hinweis hinaus. Erst dropna=False behält sie als eigene Gruppe.

dplyr und pandas

dplyr pandas
filter() .loc[maske], .query()
select() df[["a", "b"]]
mutate() .assign()
arrange() .sort_values()
group_by() mit summarise() .groupby() mit .agg()
left_join() .merge(how="left")
pivot_wider() .pivot()
pivot_longer() .melt()
n() "size"

Methodenketten funktionieren wie die Pipe in R. Runde Klammern um den ganzen Ausdruck erlauben den Zeilenumbruch, und assign mit einer lambda-Funktion greift auf den Zwischenstand zu:

ergebnis = (messungen
    .query("verbrauch > 90")
    .assign(abweichung=lambda d: d["verbrauch"] - d["verbrauch"].mean())
    .sort_values("abweichung", ascending=False))

Kurz nachgeschlagen

Aufgabe R Python
Einlesen mit Semikolon und Dezimalkomma read.csv2("d.csv") pd.read_csv("d.csv", sep=";", decimal=",")
erster Blick str(df), summary(df) df.info(), df.describe()
fehlende Werte je Spalte colSums(is.na(df)) df.isna().sum()
ersetzen tidyr::replace_na() .fillna()
Häufigkeiten table(df$x), count(df, x) df["x"].value_counts()
Schlüssel eindeutig? anyDuplicated(df$id) == 0 df["id"].is_unique
Join mit Erwartung left_join(..., relationship = "many-to-one") .merge(..., validate="many_to_one")
exportieren write.csv(df, "e.csv", row.names = FALSE) df.to_csv("e.csv", index=False)

Beispiele

Frage und Datenlage

Zwei Serien mit den Beschriftungen x, y, z, die zweite in umgekehrter Reihenfolge (z = 10, y = 20, x = 30). Was ergibt die Summe, in pandas und mit benannten Vektoren in R? Und was, wenn in der zweiten Serie z fehlt?

Rechnung

a <- c(x = 1, y = 2, z = 3)
b <- c(z = 10, y = 20, x = 30)
a + b                        # nach Position, die Namen von a bleiben
 x  y  z 
11 22 33 
a + b[names(a)]              # erst ausrichten, dann rechnen
 x  y  z 
31 22 13 
c2 <- c(x = 10, y = 20)
suppressWarnings(a + c2)     # Recycling statt fehlender Wert
 x  y  z 
11 22 13 
a = pd.Series([1, 2, 3], index=["x", "y", "z"])
b = pd.Series([10, 20, 30], index=["z", "y", "x"])
print((a + b).to_dict())                          # nach Index
{'x': 31, 'y': 22, 'z': 13}
print((a.to_numpy() + b.to_numpy()).tolist())     # nach Position
[11, 22, 33]
c = pd.Series([10, 20], index=["x", "y"])
print((a + c).to_dict())
{'x': 11.0, 'y': 22.0, 'z': nan}

Output Zeile für Zeile

Rechnung pandas R Erklärung
a + b x 31, y 22, z 13 x 11, y 22, z 33 pandas addiert x zu x. R addiert Position zu Position und übernimmt die Namen von a, obwohl dort z und x zusammengezählt wurden.
nach Position [11, 22, 33] Über .to_numpy() rechnet pandas wie R.
ausgerichtet in R x 31, y 22, z 13 b[names(a)] sortiert b nach den Namen von a.
z fehlt x 11.0, y 22.0, z NaN x 11, y 22, z 13 pandas meldet die Lücke als fehlenden Wert. R recycelt die 10 für z und warnt nur.

Die Werte in pandas werden zu Gleitkommazahlen (11.0), weil NaN eine Gleitkommazahl ist und die ganze Serie denselben Typ tragen muss.

Interpretation und Ergebnissatz

Die Ausrichtung ist der Grund, warum pandas-Code selten “danebenrechnet”, wenn zwei Tabellen verschieden sortiert sind. Sie ist zugleich der Grund für unerwartete NaN, sobald die Indizes nicht deckungsgleich sind. R vertraut auf die Reihenfolge; die Namen sind dort Beschriftung, keine Zuordnung.

pandas addiert nach Index und liefert x = 31, y = 22, z = 13, R addiert benannte Vektoren nach Position und liefert unter denselben Namen 11, 22, 33. Fehlt ein Eintrag, entsteht in pandas NaN, in R ein recycelter Wert.

Frage und Datenlage

Eine kleine Tabelle mit Index a bis d: Was liefern loc["b":"c"] und iloc[1:3]? Dann aus den 24 Messungen alle Berner Werte über 125, und die Probe, was ohne Klammern um die Teilbedingungen passiert.

Rechnung

messungen[messungen$standort == "Bern" & messungen$verbrauch > 125, ]
        datum standort verbrauch
3  2026-01-12     Bern     125.4
7  2026-01-26     Bern     130.6
13 2026-02-16     Bern     128.9
21 2026-03-16     Bern     133.2
nrow(subset(messungen, verbrauch > 130))
[1] 2
df = pd.DataFrame({"wert": [10, 20, 30, 40]}, index=["a", "b", "c", "d"])
print(df.loc["b":"c", "wert"].tolist(), df.iloc[1:3, 0].tolist())
[20, 30] [20, 30]
maske = (messungen["standort"] == "Bern") & (messungen["verbrauch"] > 125)
print(messungen.loc[maske])
        datum standort  verbrauch
2  2026-01-12     Bern      125.4
6  2026-01-26     Bern      130.6
12 2026-02-16     Bern      128.9
20 2026-03-16     Bern      133.2
print(len(messungen.loc[messungen["verbrauch"] > 130, ["datum", "verbrauch"]]))
2
try:
    messungen["standort"] == "Bern" & messungen["verbrauch"] > 125
except TypeError as fehler:
    print(type(fehler).__name__)
TypeError

Output Zeile für Zeile

Schritt Ergebnis Erklärung
loc["b":"c"] [20, 30] Beschriftungen, beide Grenzen dabei.
iloc[1:3] [20, 30] Positionen 1 und 2, die 3 ist ausgeschlossen. Dasselbe Ergebnis auf verschiedenem Weg.
Bern über 125 vier Zeilen: 125.4, 130.6, 128.9, 133.2 am 12. Januar, 26. Januar, 16. Februar und 16. März
Zeilenbeschriftung pandas 2, 6, 12, 20; R 3, 7, 13, 21 Dieselben Zeilen. pandas zählt den Index ab null, R die Zeilennamen ab eins.
über 130 2 Messungen 130.6 und 133.2
ohne Klammern TypeError Python rechnet zuerst "Bern" & messungen[...], und das ist sinnlos. R braucht die Klammern nicht, weil dort == stärker bindet als &.

Interpretation und Ergebnissatz

Die untere Grenze wird bei beiden Zugriffen gleich behandelt, die obere nicht. Das ist Absicht: Bei Beschriftungen wüsste man sonst nicht, welcher Eintrag der nächste wäre, den man ausschliessen soll. Für Bedingungen ist .loc der Zugriff der Wahl, weil er Zeilen und Spalten in einem Schritt nimmt.

loc["b":"c"] und iloc[1:3] liefern beide die Werte 20 und 30. In Bern lagen vier Messungen über 125, die höchste bei 133.2 am 16. März; ohne Klammern um die Teilbedingungen bricht der Ausdruck mit einem TypeError ab.

Frage und Datenlage

In einer Tabelle mit den Gruppen a, b, a soll der Wert aller Zeilen der Gruppe a auf 99 gesetzt werden, einmal mit zwei Zugriffen hintereinander und einmal mit einem einzigen .loc.

Rechnung

df <- data.frame(wert = c(1, 2, 3), gruppe = c("a", "b", "a"))
df$wert[df$gruppe == "a"] <- 99      # verkettet, in R gueltig
df$wert
[1] 99  2 99
df = pd.DataFrame({"wert": [1, 2, 3], "gruppe": ["a", "b", "a"]})

with warnings.catch_warnings(record=True) as meldungen:
    warnings.simplefilter("always")
    df[df["gruppe"] == "a"]["wert"] = 99          # zwei Zugriffe
print([type(m.message).__name__ for m in meldungen])
['SettingWithCopyWarning']
print(df["wert"].tolist())
[1, 2, 3]
df.loc[df["gruppe"] == "a", "wert"] = 99          # ein Zugriff
print(df["wert"].tolist())
[99, 2, 99]

Output Zeile für Zeile

Variante Warnung Werte danach Erklärung
zwei Zugriffe SettingWithCopyWarning (ab pandas 3 ChainedAssignmentError) [1, 2, 3] Die Zuweisung traf eine Kopie, die sofort verworfen wurde. Das Original ist unverändert.
ein .loc keine [99, 2, 99] Zeilen und Spalte in einem Zugriff.
R verkettet 99 2 99 In R ist df$wert[...] <- 99 eine gültige Ersetzung.

Die Warnung wird hier eingefangen und ausgegeben, weil diese Sammlung Warnungen sonst ausblendet. Im eigenen Code sollte sie nie unterdrückt werden: Sie ist der einzige Hinweis darauf, dass eine Zuweisung nichts bewirkt hat.

Interpretation und Ergebnissatz

Der Fehler ist heimtückisch, weil der Code gültig aussieht und in R genau so geschrieben würde. Bis pandas 2 hängt das Ergebnis sogar vom Fall ab, weil ein Zwischenergebnis mal eine View und mal eine Kopie ist; ab pandas 3 ist es mit copy on write eindeutig, aber eindeutig wirkungslos. Die Regel bleibt: Wer schreiben will, benutzt einen einzigen .loc-Zugriff.

Die verkettete Zuweisung liess die Werte bei 1, 2, 3 und löste eine Warnung aus; erst df.loc[bedingung, "wert"] = 99 setzte beide Zeilen der Gruppe a auf 99. In R ist die verkettete Form gültig.

Frage und Datenlage

Vier Werte 1, 2, NaN, 4: Welcher Mittelwert kommt heraus? Und eine Tabelle mit den Gruppen a, b, fehlend, a und den Werten 1 bis 4: Welche Summen je Gruppe liefert groupby?

Rechnung

x <- c(1, 2, NA, 4)
mean(x)
[1] NA
mean(x, na.rm = TRUE)
[1] 2.333333
sum(is.na(x))
[1] 1
d <- data.frame(gruppe = c("a", "b", NA, "a"), wert = 1:4)
d |> group_by(gruppe) |> summarise(summe = sum(wert))
# A tibble: 3 × 2
  gruppe summe
  <chr>  <int>
1 a          5
2 b          2
3 <NA>       3
x = pd.Series([1, 2, np.nan, 4])
print(round(x.mean(), 4), x.count(), len(x), x.isna().sum())
2.3333 3 4 1
print(x.mean(skipna=False))
nan
d = pd.DataFrame({"gruppe": ["a", "b", None, "a"], "wert": [1, 2, 3, 4]})
print(d.groupby("gruppe")["wert"].sum().to_dict())
{'a': 5, 'b': 2}
print(d.groupby("gruppe", dropna=False)["wert"].sum().to_dict())
{'a': 5, 'b': 2, nan: 3}

Output Zeile für Zeile

Grösse pandas R Erklärung
Mittelwert 2.3333 NA pandas lässt den fehlenden Wert still weg: (1 + 2 + 4) / 3. R verweigert das Ergebnis.
mit ausdrücklicher Angabe skipna=False: nan na.rm = TRUE: 2.333333 Jede Sprache kann das Verhalten der anderen, nur mit umgekehrter Voreinstellung.
vorhandene Werte count(): 3 von len(): 4 count zählt nur Vorhandenes, len alles.
fehlende Werte 1 1
Summen je Gruppe a 5, b 2 a 5, b 2, NA 3 Die Zeile mit fehlender Gruppe fehlt in pandas, ohne Hinweis. dplyr führt sie als eigene Gruppe.
mit dropna=False a 5, b 2, nan 3 Jetzt ist die Summe aller Gruppen wieder 10.

Interpretation und Ergebnissatz

Beide Stellen verlieren Daten, ohne es zu sagen. Beim Mittelwert ist das oft gewollt, beim groupby selten: Die Gruppensummen addieren sich zu 7 statt zu 10, und wer die Gesamtsumme nicht nachrechnet, bemerkt es nicht. Die Kontrolle ist einfach: Summe der Gruppen gegen Summe der Spalte, und die Anzahl fehlender Werte je Spalte vor jeder Auswertung. Mehr dazu unter Datenqualität.

pandas ergibt einen Mittelwert von 2.3333, weil der fehlende Wert still weggelassen wird; R liefert NA. Beim Gruppieren fällt in pandas die Zeile mit fehlendem Schlüssel weg, sodass sich die Summen zu 7 statt 10 addieren; erst dropna=False behält sie.

Frage und Datenlage

Anzahl, mittlerer Verbrauch und Spannweite je Standort über alle 24 Messungen, danach der mittlere Verbrauch je Monat und Standort als breite Tabelle mit einer Spalte je Standort.

Rechnung

messungen |>
  group_by(standort) |>
  summarise(n      = n(),
            mittel = round(mean(verbrauch), 2),
            spanne = round(max(verbrauch) - min(verbrauch), 1),
            .groups = "drop")
# A tibble: 2 × 4
  standort     n mittel spanne
  <chr>    <int>  <dbl>  <dbl>
1 Bern        12  123.    17.4
2 Chur        12   95.8   14.4
monat <- as.integer(format(messungen$datum, "%m"))
round(tapply(messungen$verbrauch, list(monat, messungen$standort), mean), 2)
    Bern  Chur
1 123.80 94.62
2 121.62 95.38
3 123.92 97.25
print(messungen.groupby("standort").agg(
    n=("verbrauch", "size"),
    mittel=("verbrauch", "mean"),
    spanne=("verbrauch", lambda s: s.max() - s.min()),
).round(2))
           n  mittel  spanne
standort                    
Bern      12  123.12    17.4
Chur      12   95.75    14.4
monatlich = (messungen
    .assign(monat=messungen["datum"].dt.month)
    .groupby(["monat", "standort"], as_index=False)
    .agg(mittel=("verbrauch", "mean")))
print(monatlich.pivot(index="monat", columns="standort", values="mittel").round(2))
standort    Bern   Chur
monat                  
1         123.80  94.62
2         121.62  95.38
3         123.92  97.25

Output Zeile für Zeile

Standort n Mittel Spannweite Januar Februar März
Bern 12 123.12 17.4 123.80 121.62 123.92
Chur 12 95.75 14.4 94.62 95.38 97.25
Teil Erklärung
n size in pandas, n() in dplyr: Zeilen je Gruppe, fehlende Werte eingeschlossen. count würde nur vorhandene zählen.
Mittel 1477.4 / 12 für Bern, 1149.0 / 12 für Chur
Spannweite Bern 133.2 − 115.8, Chur 103.1 − 88.7. Eine eigene Funktion als lambda in agg.
Monatsmittel je vier Wochen, weil jeder Monat hier vier Messtage hat
breite Form pivot in pandas, in R hier tapply mit zwei Gruppierungen; mit tidyr wäre es pivot_wider().

Interpretation und Ergebnissatz

groupby mit benannten Aggregationen entspricht Zeile für Zeile group_by mit summarise, nur dass pandas die Gruppen in den Index legt. Die breite Tabelle zeigt, was die Gesamtmittel verbergen: Bern bleibt über alle drei Monate nahezu gleich, Chur steigt von 94.62 auf 97.25.

Bern verbrauchte im Mittel 123.12 bei einer Spannweite von 17.4, Chur 95.75 bei 14.4, je über zwölf Messungen. Nach Monaten steigt der Verbrauch in Chur von 94.62 im Januar auf 97.25 im März, in Bern bleibt er zwischen 121.62 und 123.92.

Frage und Datenlage

Die Messungen sollen um den Kanton ergänzt werden. Die Stammdaten enthalten Bern, Chur und Sion. Eine zweite Fassung der Stammdaten führt Chur versehentlich doppelt. Was passiert mit Zeilenzahl und Gesamtverbrauch?

Rechnung

stammdaten <- data.frame(standort = c("Bern", "Chur", "Sion"),
                         kanton = c("BE", "GR", "VS"))
doppelt <- data.frame(standort = c("Bern", "Chur", "Chur"),
                      kanton = c("BE", "GR", "GR"))

c(vorher = nrow(messungen),
  nachher = nrow(left_join(messungen, stammdaten, by = "standort")),
  doppelt = nrow(left_join(messungen, doppelt, by = "standort")))
 vorher nachher doppelt 
     24      24      36 
round(c(original = sum(messungen$verbrauch),
        nach_join = sum(left_join(messungen, doppelt, by = "standort")$verbrauch)), 1)
 original nach_join 
   2626.4    3775.4 
anyDuplicated(doppelt$standort)
[1] 3
inherits(tryCatch(left_join(messungen, doppelt, by = "standort",
                            relationship = "many-to-one"),
                  error = function(e) e), "error")
[1] TRUE
stammdaten = pd.DataFrame({"standort": ["Bern", "Chur", "Sion"],
                           "kanton": ["BE", "GR", "VS"]})
doppelt = pd.DataFrame({"standort": ["Bern", "Chur", "Chur"],
                        "kanton": ["BE", "GR", "GR"]})

verknuepft = messungen.merge(stammdaten, on="standort", how="left")
falsch = messungen.merge(doppelt, on="standort", how="left")
print(len(messungen), len(verknuepft), len(falsch))
24 24 36
print(round(messungen["verbrauch"].sum(), 1), round(falsch["verbrauch"].sum(), 1))
2626.4 3775.4
print(stammdaten["standort"].is_unique, doppelt["standort"].is_unique)
True False
try:
    messungen.merge(doppelt, on="standort", how="left", validate="many_to_one")
except pd.errors.MergeError as fehler:
    print(type(fehler).__name__)
MergeError
aussen = messungen.merge(stammdaten, on="standort", how="outer")
print(aussen["standort"].value_counts().to_dict())
{'Bern': 12, 'Chur': 12, 'Sion': 1}

Output Zeile für Zeile

Schritt Python R Erklärung
Zeilen vorher 24 24
nach Join mit eindeutigen Stammdaten 24 24 Jede Messung findet genau einen Kanton. Sion hat keine Messung und fällt beim Left Join weg.
nach Join mit doppeltem Chur 36 36 Jede der zwölf Churer Messungen wird mit beiden Chur-Zeilen kombiniert.
Gesamtverbrauch 2626.4 gegen 3775.4 dieselben Der Churer Verbrauch von 1149.0 zählt doppelt.
Schlüssel eindeutig? True, False anyDuplicated: 3 R nennt die Position der ersten Doppelung.
Join mit Erwartung MergeError Fehler (TRUE) validate="many_to_one" beziehungsweise relationship = "many-to-one" bricht ab, statt still zu vervielfachen.
Outer Join Bern 12, Chur 12, Sion 1 Ein Outer Join fügt fehlende Schlüssel als Zeile mit NaN hinzu, vervielfacht aber nichts.

Interpretation und Ergebnissatz

Ein Join mit doppeltem Schlüssel erzeugt keinen Fehler, sondern eine grössere Tabelle, und jede Summe danach ist falsch. Die Gewohnheit, nach jedem Join die Zeilenzahl mit der vorherigen zu vergleichen, kostet eine Zeile Code. Noch besser ist es, die Erwartung gleich in den Aufruf zu schreiben: validate in pandas, relationship in dplyr. Warum Schlüssel eindeutig sein müssen, erklärt die Datenmodellierung.

Mit eindeutigen Stammdaten behält die Tabelle ihre 24 Zeilen. Ist Chur in den Stammdaten doppelt, wächst sie ohne Fehlermeldung auf 36 Zeilen, und der Gesamtverbrauch steigt von 2626.4 auf 3775.4; validate="many_to_one" hätte den Join abgebrochen.

Verständnisfragen

Warum schliesst df.loc["b":"c"] die Grenze ein, df.iloc[1:3] aber nicht?

.loc arbeitet mit Beschriftungen, .iloc mit Positionen
Richtig. Bei Beschriftungen wäre eine ausgeschlossene obere Grenze unbrauchbar, weil man den nächsten Eintrag kennen müsste.
Es ist eine Inkonsistenz ohne Grund
Der Unterschied ist beabsichtigt und folgt aus der Art der Angabe.
.loc ist die ältere Schreibweise
Beide sind aktuell und für verschiedene Zwecke gedacht.

Zwei Series mit Index x, y, z und z, y, x werden addiert. In R ergäben benannte Vektoren 11, 22, 33. Was ergibt pandas?

x 31, y 22, z 13
Richtig. pandas richtet nach Index aus und addiert x zu x. R rechnet nach Position.
Ebenfalls 11, 22, 33
Das wäre das Verhalten von NumPy-Arrays oder .to_numpy().
Einen Fehler wegen unterschiedlicher Reihenfolge
Der Fall ist vorgesehen und läuft still durch.

Warum ist df[df["a"] > 1]["b"] = 0 wirkungslos?

Die Zuweisung trifft ein Zwischenergebnis, das danach verworfen wird
Richtig. pandas warnt dabei mit SettingWithCopyWarning, ab Version 3 mit ChainedAssignmentError. Ein einziger .loc-Zugriff umgeht das.
Die Bedingung ist falsch geschrieben
Die Bedingung ist in Ordnung.
Es funktioniert nur bei numerischen Spalten
Der Datentyp spielt keine Rolle.

Die Summen aus df.groupby("gruppe")["wert"].sum() ergeben zusammen 7, die Spalte wert summiert 10. Was ist passiert?

Zeilen mit fehlendem Gruppenschlüssel wurden weggelassen
Richtig. groupby verwirft sie ohne Hinweis. dropna=False behält sie als eigene Gruppe, wie Beispiel 4 zeigt.
sum hat einen fehlenden Wert als 0 gezählt
Das würde die Summe nicht verringern.
Ein Rundungsfehler
Bei ganzen Zahlen gibt es keinen.

Nach einem merge hat die Tabelle mehr Zeilen als vorher. Was ist passiert?

Der Schlüssel war in der rechten Tabelle nicht eindeutig
Richtig. Jede Zeile links wurde mit jeder passenden Zeile rechts kombiniert. Vor dem Join lohnt der Blick auf is_unique, oder gleich validate="many_to_one".
Es wurde versehentlich ein Outer Join verwendet
Der fügt fehlende Schlüssel hinzu, vervielfacht aber keine vorhandenen Zeilen.
pandas hat die Zeilen doppelt eingelesen
Der Einlesevorgang ist nicht beteiligt.

In pandas liefert x.mean() bei einem fehlenden Wert eine Zahl, in R mean(x) den Wert NA. Welche Voreinstellung ist die vorsichtigere?

Die von R, weil sie zur Entscheidung zwingt
Richtig. Wer na.rm = TRUE schreibt, weiss, dass Werte fehlen. In pandas muss man daran denken, isna().sum() selbst aufzurufen.
Die von pandas, weil sie ein Ergebnis liefert
Ein Ergebnis ohne Hinweis auf fehlende Werte kann täuschen.
Beide sind gleichwertig
Das Ergebnis mit na.rm = TRUE ist gleich, der Weg dorthin nicht.

Verlinkte Ressourcen