Skip to content

pandas loc

Select rows and columns by label — and, most often, by a condition.

.loc is how you say "the rows where this is true" and "these columns, by name". It is also the piece that makes method chaining work, because .loc accepts a deferred expression, letting you filter a data frame that has no name yet.

Official documentation: DataFrame.loc

The forms worth knowing

df.loc['Japan']                       # one row, by index label
df.loc['Japan', 'Capacity (MW)']      # one cell: row label, column label
df.loc[:, ['Country', 'Status']]      # all rows, two columns by name
df.loc[pd.col('mag') > 7]             # boolean condition
df.loc[pd.col('mag') > 7, 'place']    # condition, and one column
df.loc['a':'c']                       # label slice -- 'c' IS included

That last one catches people: unlike ordinary Python slicing, a .loc slice includes its endpoint, because you are naming labels rather than counting positions.

pd.col, and why it matters

Inside a chain, the intermediate data frame has no variable name — so you cannot write df[df['x'] > 5], because there is no df to refer to. pd.col, introduced in Pandas 3.0, solves this: it is a deferred reference to a column, resolved against whatever the chain has produced at that point.

(
    df
    .loc[pd.col('Available Globally?') == 'Yes']
    .loc[pd.col('Hours Viewed') > 500_000_000]
)

Each .loc filters what the previous one produced, so conditions stack readably rather than piling into one enormous boolean expression. pd.col works wherever a column reference makes sense — including inside assign:

df.assign(hours_millions=pd.col('Hours Viewed') / 1_000_000)

Conditions combine with & and |, each side parenthesized, and the usual methods are available:

df.loc[(pd.col('mag') > 7) & (pd.col('depth') < 50)]
df.loc[pd.col('Region').isin(['Asia', 'Europe'])]

A note on older code

Before Pandas 3.0 the same job was done with a lambda, which .loc also accepts:

df.loc[lambda df_: df_['Hours Viewed'] > 500_000_000]

The df_ name was a convention meaning "the frame at this point in the chain". You will see this form throughout the older Bamboo Weekly archive, and it still works — but pd.col says the same thing with less ceremony, and is the form to reach for in new code.

If you are reading older code and want the lambda form explained properly, see Selecting rows in Pandas using .loc and lambda.

A worked example, on real data

Netflix published an engagement report listing every title watched over six months, with hours viewed and whether it was available worldwide. Bamboo Weekly #45 used it.

The question: which globally available titles passed 500 million hours viewed?

import pandas as pd

url = ('https://www.bambooweekly.com/content/files/4cd45et68cgf/1HyknFM84ISQpeua6TjM7A/'
       '97a0a393098937a8f29c9d29c48dbfa8/'
       'what_we_watched_a_netflix_engagement_report_2023jan-jun.xlsx')

(
    pd.read_excel(url, skiprows=5,
                  usecols=['Title', 'Available Globally?', 'Release Date', 'Hours Viewed'])
    .loc[pd.col('Available Globally?') == 'Yes']
    .loc[pd.col('Hours Viewed') > 500_000_000]
    .sort_values('Hours Viewed', ascending=False)
    .head(5)
)

Which gives:

                             Title Available Globally? Release Date  Hours Viewed
         The Night Agent: Season 1                 Yes   2023-03-23     812100000
         Ginny & Georgia: Season 2                 Yes   2023-01-05     665100000
The Glory: Season 1 // 더 글로리: 시즌 1                 Yes   2022-12-30     622800000
               Wednesday: Season 1                 Yes   2022-11-23     507700000
Queen Charlotte: A Bridgerton S...                 Yes   2023-05-04     503000000

Five rows out of 18,214, with no intermediate variables and each filtering step readable on its own line.

Three mistakes people make

Chained indexing, which in Pandas 3.0 silently does nothing. Writing df[df['a'] > 1]['b'] = 99 raises a ChainedAssignmentError warning and leaves your data frame completely unchanged — the assignment lands on a temporary object that is discarded. This is the single most dangerous habit in Pandas, because your code appears to run. Say it in one .loc instead:

df.loc[pd.col('a') > 1, 'b'] = 99

This is the Pandas 3 behavior of copy-on-write, which replaced the old SettingWithCopyWarning. I explain what changed and why in SettingWithCopyWarning? Not in Pandas 3, thanks to "copy on write".

Confusing .loc with .iloc. .loc uses labels, .iloc uses integer positions. They coincide on a default RangeIndex, which lets the confusion survive until the day you sort or filter and the labels stop matching the positions.

Assuming a .loc slice stops before the endpoint. df.loc['a':'c'] returns a, b and c. Python slicing excludes the end; label-based slicing includes it, since Pandas cannot know what comes "just before" a label. I demonstrate both of those last two in How .loc and .iloc treat Pandas slices differently. And on why the syntax is square brackets rather than parentheses: Why we call .loc with [] and not ().

Practice it

Work through a .loc exercise, with instant feedback and no signup required: practice.lernerpython.com/bamboo-weekly/loc/

Go deeper

Bamboo Weekly is the practice. If you want the structured version — full courses with downloadable Jupyter notebooks, plus live Pandas office hours when you get stuck — that is LernerPython+Data. A paid Bamboo Weekly subscription is included with it.

See it on real data

Below are the 159 Bamboo Weekly exercises that use loc on real-world data — try each one, then study the worked solution.

Part of the Pandas Methods Index. See also practice by skill.