pandas gives you two indexers because a DataFrame has two coordinate systems. .loc selects by label — the names in the index and the column headers. .iloc selects by integer position — 0-based, like a Python list. Same table, two ways to point at a cell.
Where you meet it
- Grabbing rows by a meaningful index — a date, a ticker, a user ID — with
.loc. - Walking a frame by raw order — "the first 100 rows", "every other column" — with
.iloc. - Writing safe assignments:
df.loc[mask, 'col'] = ...instead of chained indexing. - Splitting features and target in a model:
X = df.iloc[:, :-1],y = df.iloc[:, -1].
What it does
Both take a [rows, cols] pair and return the intersection. The difference is only what the keys mean: .loc reads them as labels, .iloc as positions. That choice also changes how slices behave — and that is where most bugs live.
.loc slices include the end label; .iloc stops one short. That off-by-one is where the bugs hide.
How it works
The general form is df.loc[rows, cols] (or .iloc). Pass a single key, a list, a slice, or a boolean mask on either axis. The two indexers slice in opposite ways:
import pandas as pd
df = pd.DataFrame(
{'age': [34, 28, 41, 23, 37]},
index=['u1', 'u2', 'u3', 'u4', 'u5'],
)
# .loc — by label, end label is INCLUDED
df.loc['u2':'u4', 'age'] # u2, u3, u4 → 3 rows
# .iloc — by position, end is EXCLUDED (like Python)
df.iloc[1:3, 0] # positions 1, 2 → 2 rows
The pandas docs are blunt about it: for .loc, "contrary to usual Python slices, both the start and the stop are included." For .iloc, "the start bound is included, while the upper bound is excluded."
Watch out
- The
.locslice is end-inclusive.df.loc['u2':'u4']returns three rows, not two — the single biggest surprise when switching from list slicing. - Chained indexing.
df[df.bar > 5]['foo'] = 100assigns through two steps and silently fails (aSettingWithCopyWarning, or under Copy-on-Write aChainedAssignmentError). Write it asdf.loc[df.bar > 5, 'foo'] = 100. - Integer labels are a trap. If the index is integers but not
0,1,2,…,.loc[2]looks up the label 2 while.iloc[2]takes the third row — usually different rows. - Don't index with bare
[]for cells.df['age']selects a column; for a row, or a row-and-column pair, reach for.loc/.ilocexplicitly.
Go deeper
pandas hat zwei Indexer, weil ein DataFrame zwei Koordinatensysteme hat. .loc wählt per Label — die Namen im Index und die Spaltenköpfe. .iloc wählt per Integer-Position — ab 0, wie eine Python-Liste. Dieselbe Tabelle, zwei Wege, auf eine Zelle zu zeigen.
Wo es vorkommt
- Zeilen über einen sinnvollen Index holen — ein Datum, ein Ticker, eine User-ID — mit
.loc. - Den Frame über die rohe Reihenfolge durchgehen — "die ersten 100 Zeilen", "jede zweite Spalte" — mit
.iloc. - Sichere Zuweisungen schreiben:
df.loc[mask, 'col'] = ...statt chained indexing. - Features und Target im Modell trennen:
X = df.iloc[:, :-1],y = df.iloc[:, -1].
Was es tut
Beide nehmen ein [zeilen, spalten]-Paar und liefern die Schnittmenge. Der Unterschied liegt nur darin, was die Schlüssel bedeuten: .loc liest sie als Labels, .iloc als Positionen. Diese Wahl ändert auch, wie sich Slices verhalten — und genau da sitzen die meisten Bugs.
.loc-Slices schließen das End-Label ein, .iloc hört eins früher auf. In diesem Off-by-one sitzen die Bugs.
Wie es funktioniert
Die allgemeine Form ist df.loc[zeilen, spalten] (oder .iloc). Übergib einen Einzelschlüssel, eine Liste, einen Slice oder eine boolesche Maske auf jeder Achse. Die beiden Indexer slicen entgegengesetzt:
import pandas as pd
df = pd.DataFrame(
{'age': [34, 28, 41, 23, 37]},
index=['u1', 'u2', 'u3', 'u4', 'u5'],
)
# .loc — per Label, End-Label INKLUSIVE
df.loc['u2':'u4', 'age'] # u2, u3, u4 → 3 Zeilen
# .iloc — per Position, Ende EXKLUSIV (wie Python)
df.iloc[1:3, 0] # Positionen 1, 2 → 2 Zeilen
Die pandas-Doku sagt es klar: bei .loc sind "im Gegensatz zu üblichen Python-Slices beide — Start und Stop — eingeschlossen". Bei .iloc ist "die untere Grenze eingeschlossen, die obere ausgeschlossen".
Worauf achten
- Der
.loc-Slice ist end-inklusive.df.loc['u2':'u4']liefert drei Zeilen, nicht zwei — die größte Überraschung beim Umstieg vom Listen-Slicing. - Chained Indexing.
df[df.bar > 5]['foo'] = 100weist über zwei Schritte zu und schlägt still fehl (eineSettingWithCopyWarning, unter Copy-on-Write einChainedAssignmentError). Schreib es alsdf.loc[df.bar > 5, 'foo'] = 100. - Integer-Labels sind eine Falle. Ist der Index selbst ganzzahlig, aber nicht
0,1,2,…, sucht.loc[2]das Label 2, während.iloc[2]die dritte Zeile nimmt — meist verschiedene Zeilen. - Für Zellen nicht das nackte
[]nutzen.df['age']wählt eine Spalte; für eine Zeile oder ein Zeilen-Spalten-Paar greif bewusst zu.loc/.iloc.