In practice, columns are rarely left with default numeric labels like 0, 1, 2, 3. They're assigned meaningful names that reflect the data they contain. When you need to reference, filter, or extract values using these descriptive column names, you'll use LOC (location), not ILOC (integer location). This distinction is fundamental to effective data manipulation in pandas.
Think of LOC as "named location" — it works with the human-readable labels you've assigned to your data structure. While row names often mirror their index numbers (making LOC and ILOC interchangeable for rows), column names typically don't follow this pattern. Consider our food dataframe: instead of numeric columns 0, 1, 2, 3, we have meaningful labels like "item," "price," "calories," and "vegan." When querying for price data, you shouldn't need to remember that "price" is internally stored as column 1 — LOC lets you work with intuitive names.
Let's return to our chessboard example to demonstrate this concept in action. We'll modify the rook pieces from "R" to "RK" to practice targeting cells using named references rather than numeric indices.
Notice how our chessboard's structure mirrors real-world data labeling. Row 0 in ILOC corresponds to row "8" in LOC, while column 0 in ILOC maps to column "A" in LOC. Previously, we've been working with numeric indices — ignoring the traditional chess notation of letters A-H for columns and the reverse numbering 8-1 for rows. Instead of limiting ourselves to 0-7 indexing for both dimensions, we can leverage the descriptive labels that make our code more readable and maintainable.
Here's how we target the rooks using named locations. We'll change the existing "R" symbols to "RK" using LOC instead of ILOC, referencing rows and columns by their chess notation names: A-H for columns and 8-1 for rows, rather than the underlying 0-7 numeric indices.
The syntax is straightforward: chessboarddf.loc[row_labels, column_labels]. For our rook modification, we want rows 8 and 1 (the top and bottom ranks) and columns A and H (the corner files). Instead of writing separate commands for each row as we did previously, we can target both simultaneously: chessboarddf.loc[[8,1], ['A','H']] = 'RK'. This efficiently updates all four corner positions in a single operation.
Perfect — we now see "RK" in all four corner positions. Let's practice with another piece type. Try changing the bishops from "B" to "BP" using the same LOC approach. Take a moment to work through this challenge before continuing.
The solution targets the same rows (8 and 1 for top and bottom ranks) but different columns. Bishops occupy the C and F files, not the corner A and H positions of the rooks: chessboarddf.loc[[8,1], ['C','F']] = 'BP'. This demonstrates how LOC's named referencing makes the code self-documenting — anyone reading this code immediately understands we're modifying pieces on specific chess squares.
For our final chess example, let's modify the pawns from "P" to "PN". This presents a slightly different challenge since pawns occupy entire ranks rather than specific squares. The pawns sit on ranks 7 and 2 (using chess notation), spanning all files from A to H.
Here's the key insight: when we want non-contiguous rows (7 and 2, which don't touch), we pass them as a list. For columns, since we want all files, we use a colon to indicate the full range: chessboarddf.loc[[7,2], :] = 'PN'. The colon serves as a wildcard, selecting all columns while our bracketed list targets specific, non-adjacent rows.
Now let's examine all three modifications together to solidify the LOC concept. We've successfully used named references instead of numeric indices: letters A-H rather than the underlying 0-7 column indices, and chess rank numbers rather than their corresponding array positions. This approach makes our code more intuitive and less prone to off-by-one errors that plague numeric indexing.
Returning to our food dataframe, let's contrast ILOC and LOC approaches side by side. First, we'll use ILOC to extract the first three rows and first three columns — deliberately excluding the "vegan" column and the "garden salad" row to create a focused subset.
With ILOC, we specify numeric ranges: fooddf.iloc[0:3, 0:3]. Remember that ILOC uses exclusive upper bounds, so 0:3 actually returns indices 0, 1, and 2. This gives us exactly what we want: no vegan column, no garden salad row.
Now here's where LOC behavior differs significantly. LOC uses inclusive upper bounds, meaning if you specify row 3, you'll get everything up to and including row 3. For our equivalent LOC operation, we need fooddf.loc[0:2, 'item':'calories']. Notice two critical differences: we stop at row 2 (not 3) because LOC is inclusive, and we must use column names ('item':'calories') rather than numeric indices.
This inclusive versus exclusive behavior is a common source of confusion for data professionals transitioning between the two methods. Take time to internalize this difference — ILOC excludes the upper bound (like Python's standard range function), while LOC includes it. This isn't just a syntax quirk; it reflects the fundamental difference between numeric indexing and named referencing.
Let's practice with a focused exercise. First, use ILOC to get the first two rows and first two columns from our food dataframe. Then replicate this exact selection using LOC. Work through both challenges before checking the solutions.
The ILOC solution is straightforward: fooddf.iloc[:2, :2]. This returns rows 0 and 1 with columns "item" and "price" — the first two in each dimension.
For the LOC equivalent, rows remain the same since we never assigned custom row names (the default numeric labels serve as both index and name). However, columns require their actual names: fooddf.loc[:2, 'item':'price']. We're creating a range from 'item' to 'price', and because LOC is inclusive, this captures exactly the first two columns.
Now let's explore non-contiguous selections — a powerful feature for extracting specific, non-adjacent data points. Suppose we want the first two rows but only the "item" and "calories" columns, deliberately skipping "price." This requires a different syntax since we can't use a colon (which creates ranges) for non-touching elements.
For non-contiguous selections, wrap your targets in lists: fooddf.loc[:1, ['item', 'calories']]. The square brackets indicate we're selecting specific, individual elements rather than a continuous range. This same principle applied to our chess bishops and rooks — pieces that occupy specific squares rather than continuous ranks or files.
The ILOC version follows the same pattern with numeric indices: fooddf.iloc[:2, [0, 2]]. We're selecting columns 0 and 2 (skipping column 1) using list notation for the non-contiguous column selection.
For our final example, let's select non-contiguous rows as well as columns — specifically the first and last rows with only the "item" and "calories" columns. This demonstrates the full flexibility of both approaches when working with scattered data points.
Using ILOC: fooddf.iloc[[0, 3], [0, 2]] — both dimensions require lists since we're skipping elements in both rows and columns. The LOC equivalent: fooddf.loc[[0, 3], ['item', 'calories']] — same list structure, but using meaningful names for columns while keeping the numeric row references (since our rows lack custom labels).
Mastering the distinction between LOC and ILOC is essential for efficient data manipulation. Remember the core principle: ILOC works with integer positions (the underlying numeric structure), while LOC operates with labels (the human-readable names you've assigned). This isn't merely a syntax preference — it's about writing maintainable, self-documenting code that remains robust as your datasets evolve and grow in complexity.