apply, and When Not To
Overview
A general escape hatch that is slower than the thing it usually replaces.
What You Will Learn in This Lesson
By the end of this lesson, you will be able to:
- Say what
applydoes and what it costs. - Reach for
.strand.dtmethods before reaching for apply. - Recognise the cases where apply genuinely is the answer.
- Rewrite an apply as a vectorised expression.
Why This Matters
apply is the first thing people find, because it lets you write ordinary Python over a column. It is also a Python loop wearing a pandas coat, and on a large table it can be a hundred times slower than the built-in it replaced. Knowing what it costs is what makes reaching for it a choice rather than a habit.
Step 1: apply runs your function per value
It is flexible and it is a Python loop underneath, so it costs far more than the vectorised operation it usually replaces.
What it does
Your function is called once per element, in Python, and the results are collected into a new Series.
import pandas as pd
scores = pd.Series([92, 88, 79])
print(scores.apply(lambda s: s + 5))
print(scores + 5) # the same answer, without the loop
axis=1 is the expensive one
Applying across rows builds a Series object per row and calls your function with it. On a large table this is the slowest thing in this course.
import pandas as pd
df = pd.DataFrame({"a": [1, 2], "b": [10, 20]})
print(df.apply(lambda r: r["a"] + r["b"], axis=1))
print(df["a"] + df["b"]) # the same answer, vectorised
Mini Practice #1: The same answer, two ways
Try It YourselfDouble this column with apply and then without it, and check the results match.
Step 2: Reach for the built-in first
Arithmetic, comparisons, .str methods and .dt methods all have vectorised forms. Almost every apply people write is one of these.
The .str accessor
Every string method you know, applied to a whole column, without a loop and without apply.
import pandas as pd
names = pd.Series([" Ada ", "GRACE"])
print(names.str.strip().str.lower())
print(names.str.len())
print(names.str.contains("A"))
Choosing between values
np.where and .map cover the two cases people usually reach for apply to solve: a two-way choice, and a lookup.
import pandas as pd
import numpy as np
scores = pd.Series([92, 60])
print(np.where(scores >= 70, "pass", "fail"))
codes = pd.Series(["L", "Y"])
print(codes.map({"L": "Leeds", "Y": "York"}))
When apply really is the answer
When the operation genuinely has no vectorised form — parsing an awkward string format, calling an external function, anything with real branching per row — apply is the right tool and there is nothing to feel bad about. It is the unconsidered apply that costs.
import pandas as pd
def band(row):
if row["score"] > 90 and row["city"] == "L":
return "local star"
return "other"
df = pd.DataFrame({"score": [92, 92], "city": ["L", "Y"]})
print(df.apply(band, axis=1).tolist())
Mini Practice #2: Rewrite without apply
Try It YourselfEach of these uses apply for something with a built-in. Rewrite all three.
Step 3: When to Use It
Logic with no vectorised form
Parsing an odd string format or calling a function that only takes one value at a time.
df["initials"] = df["name"].apply(to_initials)
A quick one-off on a small table
When readability matters more than speed and the table is a few hundred rows.
When Not to Use It
Do not use apply for arithmetic, comparisons or string methods that pandas already vectorises (.str, np.where, plain operators). It is a loop in disguise and much slower.
End-of-Lesson Exercises
Exercise 1: Clean a column of names
Write clean_names(names) that takes a Series of names and returns a list with the spaces trimmed and everything lowercased. Use the vectorised string methods rather than apply.
names.str.strip().str.lower() does both steps to the whole column, and .tolist() hands back plain strings.
Exercise 2: Pass or fail, without a loop
Write verdicts(scores, mark) returning a list of "pass" or "fail" for each score, using a vectorised two-way choice rather than apply.
np.where(scores >= mark, "pass", "fail") makes the whole column at once. Finish with .tolist() so the result is plain Python strings rather than numpy ones.