Method chaining
One expression, no half-modified frames, and the same answer every time you run it.
chaining.assign().pipe().query().loc[lambda]inplace is a trapWatch it happen
Play it through, or step back and forth yourself.
df = orders.copy()
df = df.drop_duplicates()
df["item"] = df["item"].str.strip().str.lower()
df = df.dropna(subset=["rating"])
df["price"] = df["revenue"] / df["cups"]
df = df[df["cups"] > 90]
df = df.sort_values("cups", ascending=False)
df = df.reset_index(drop=True)Eight steps, each reassigning df. It works, and it has three real problems: you can't tell what df holds at any given line, re-running a cell out of order silently gives a different answer, and the name stops describing the thing.
The idea
Nearly every pandas method returns a new frame. That's not an accident — it's what makes them composable, and it's the whole basis of this lesson.
The problem with the usual style
df = orders.copy()
df = df.drop_duplicates()
df["item"] = df["item"].str.strip().str.lower()
df = df.dropna(subset=["rating"])
df = df[df["cups"] > 90]It works, and it has three real problems.
You can't tell what df holds at any given line without reading every line above it.
Re-running one cell changes the answer. In a notebook, running the drop_duplicates line twice is harmless — running the filter twice isn't obviously so, and running them out of order gives a different result with no error.
The name stops describing the thing. By line five, df is a deduplicated, cleaned, filtered subset — and it's still called df.
The same thing as a chain
clean = (
orders
.drop_duplicates()
.assign(item=lambda d: d["item"].str.strip().str.lower())
.dropna(subset=["rating"])
.assign(price=lambda d: d["revenue"] / d["cups"])
.query("cups > 90")
.sort_values("cups", ascending=False)
.reset_index(drop=True)
)One expression, read top to bottom as a list of steps. There is no intermediate state to get wrong, nothing half-modified, and re-running it always gives the same answer. The wrapping parentheses are what let each step sit on its own line.
The three tools that make it work
assign adds a column and returns a new frame, so it chains where df["x"] = ... can't. The lambda matters: d is the frame as it is at that point in the chain, so a step can use a column an earlier step just created.
query — or .loc[lambda d: ...] — filters without needing a name. Mid-chain there's nothing to write df[df["cups"] > 90] against.
pipe drops any function into a chain. It calls fn(df) and passes the result on, so your own helpers join in rather than breaking the chain in half:
def add_price(df):
return df.assign(price=df["revenue"] / df["cups"])
orders.pipe(add_price).pipe(tidy_names).query("cups > 90")Debugging a chain
A chain is one expression, so you can't drop a print in the middle. Two ways round it:
.pipe(lambda d: (print(d.shape), d)[1]) # peek and pass through
orders.head(20).pipe(...) # develop on a sample firstAnd it's fine to break a chain up while you're working on it and reassemble it afterwards. The chain is the finished form, not the working one.
When not to chain
Past about eight steps a chain stops being clearer than what it replaced. Split it into named stages — each a function you can name, test and reuse:
clean = raw.pipe(deduplicate).pipe(normalise_text).pipe(add_derived)Also: if you need an intermediate result twice, name it. Computing it inside two separate chains does the work twice and invites them to drift apart.
A word on inplace
You'll see inplace=True in older tutorials. Avoid it. It returns None, so it breaks every chain and turns df = df.drop(..., inplace=True) into a silent way of destroying your frame. It doesn't reliably save memory either — pandas usually copies internally regardless. The pandas team has discussed deprecating it for years, and the ecosystem has already moved on.
See it run
The lesson's code, ready to run and to fiddle with.
Putting the kettle on…
Starting up…
Worked example
not gradedAlready written and ready to go — press Run to see what it does, then change a number, a column name, anything, and run it again.
tryadding a step of your own — .assign(big=lambda d: d["cups"] > 120).
Your turn
4 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.
In a single chain, add a price column (revenue / cups) to orders and return the frame — without assigning to a temporary.
Chain it: drop duplicates, then keep only rows with more than 100 cups, then reset the index.
One chain: add price, then keep only rows where price is above 20. The second step has to see the column the first one made.
Use .pipe() to apply len to orders after dropping duplicates — returning the row count.
