Python Pandas: Select Rows and Columns with loc and iloc
python pandas select rows columns loc and iloc: Learn how to select rows and columns in pandas using loc and iloc, including label-based and position-based indexing, w...
When working with pandas DataFrames, selecting specific rows and columns is a routine operation. The loc and iloc indexers provide the two fundamental ways to do this: loc works with labels, while iloc works with integer positions. Understanding how to use python pandas select rows columns loc and iloc correctly avoids subtle bugs and keeps your data manipulation code readable.
Understanding loc: Label-Based Selection
The loc indexer selects data by label. It accepts row and column labels, and the syntax is df.loc[row_selection, column_selection]. Both selections can be a single label, a list of labels, a slice of labels, or a boolean array.
Consider a DataFrame with a custom index:
import pandas as pd df = pd.DataFrame({ 'name': ['Alice', 'Bob', 'Charlie'], 'age': [25, 30, 35], 'city': ['NYC', 'LA', 'SF'] }, index=['a', 'b', 'c'])
To select a single row by its label, use df.loc['b']. To select multiple rows, pass a list: df.loc[['a', 'c']]. Slicing with labels is inclusive of both endpoints: df.loc['a':'b'] returns rows a and b.
You can also select columns by label. For example, df.loc[:, 'name'] returns the entire name column. Combining row and column selections is straightforward: df.loc[['a', 'c'], ['name', 'age']] returns a sub-DataFrame with the specified rows and columns.
Understanding iloc: Position-Based Selection
The iloc indexer selects data by integer position, starting at 0. The syntax is identical to loc, but both row and column selections must be integers, lists of integers, or slices of integers. Slicing with iloc is exclusive of the endpoint, matching Python's standard list slicing.
Using the same DataFrame:
# Select the first row (position 0) print(df.iloc[0]) # Select rows at positions 0 and 2 print(df.iloc[[0, 2]]) # Select rows from position 0 to 1 (exclusive of 2) print(df.iloc[0:2]) # Select specific rows and columns by position print(df.iloc[[0, 2], [0, 1]])
iloc does not accept label-based selections. Attempting to pass a label like 'b' will raise a TypeError. This strictness makes iloc predictable when you know the exact order of your data.
Key Differences Between loc and iloc
The choice between loc and iloc depends on whether you are working with labels or positions. The table below summarizes the main differences:
| Feature | loc | iloc |
|---|---|---|
| Selection basis | Label | Integer position |
| Slicing end | Inclusive | Exclusive |
| Boolean mask | Directly accepted | Requires conversion to positions |
| Scalar access | .at | .iat |
| Use with non-unique index | Allowed, returns all matches | Not affected by index labels |
loc can accept a boolean array directly, which is useful for filtering rows based on a condition. iloc does not accept a boolean array directly; you must convert it to integer indices first, for example with np.where or list(mask.index[mask]). In practice, you will rarely need to do this because loc with a boolean mask is the idiomatic way to filter rows.
Selecting Rows and Columns Together
The real power of loc and iloc appears when you combine row and column selections in a single operation. This avoids the need to chain multiple indexing steps and keeps your code concise.
For example, to select the name and city columns for rows where age is greater than 30:
print(df.loc[df['age'] > 30, ['name', 'city']])
Here, df['age'] > 30 produces a boolean Series that loc uses to filter rows. The column selection is a list of labels. This pattern is common in data cleaning and analysis.
With iloc, you can select by position. For instance, to get the first two rows and the first column:
print(df.iloc[:2, :1])
This returns a DataFrame with the first two rows and the name column. Using slices with iloc is efficient and mirrors NumPy's indexing behavior.
Common Pitfalls and Edge Cases
One of the most frequent mistakes is using chained indexing to set values. For example, df[df['age'] > 30]['age'] = 0 may not modify the original DataFrame and can trigger a SettingWithCopyWarning. The correct approach is to use loc:
df.loc[df['age'] > 30, 'age'] = 0
This single operation performs the selection and assignment in one step, avoiding the warning and ensuring the intended modification.
Another pitfall is mixing label and position selectors. You cannot use a label with iloc or a position with loc. For example, df.loc[0] will raise a KeyError if the index does not contain the integer 0. Always be aware of whether your index is meaningful or just a default RangeIndex.
Slicing with loc on a non-monotonic index can raise a KeyError or produce unexpected results. If your index is not sorted, consider using sort_index() before slicing, or use iloc with positions instead.
Performance and Best Practices for Indexing
For most operations, loc and iloc are efficient. However, if you need to access a single scalar value repeatedly, use .at for label-based and .iat for position-based access. These methods are faster than loc and iloc because they avoid the overhead of creating a Series or DataFrame.
# Fast scalar access value = df.at['b', 'age'] value = df.iat[1, 1]
When filtering rows with a boolean mask, loc is the preferred method because it is both readable and efficient. Avoid chained indexing, as it can be slower and less reliable. If you find yourself using iloc with a boolean mask, convert the mask to indices first, but consider whether loc would be simpler.
Finally, remember that iloc is slightly faster than loc for positional access because it does not need to look up labels. The difference is negligible for small DataFrames but can matter in tight loops over large datasets. Choose the indexer that matches your data's semantics rather than optimizing prematurely.