Python for Data course menu
pandas apply, map and Custom Functions (and When to Avoid Them)
Use your own functions in pandas: Series.map with dictionaries and functions, Series.apply, DataFrame.apply across rows and columns, pipe for readable pipelines, and why vectorised code is preferred when it exists.
On this page
Sometimes no built-in method does exactly what you need. pandas lets you run your own function on every value, every row or every column. These tools are flexible, but they run Python code once per item, so they are slower than vectorised operations. Use them when a vectorised version does not exist.
Series.map: translate values#
map with a dictionary replaces each value with the matching entry; values not in the dictionary become NaN:
import pandas as pd
grades = pd.Series(["A", "B", "A", "C", "E"])
points = grades.map({"A": 10, "B": 8, "C": 6, "D": 4})
print(points.tolist())
print(grades.map(str.lower).tolist()) # a function works too[10.0, 8.0, 10.0, 6.0, nan]
['a', 'b', 'a', 'c', 'e']"E" is not in the dictionary, so it maps to NaN, which is a useful way to spot unexpected values.
Series.apply: a function per value#
import pandas as pd
def delivery_band(minutes):
if minutes <= 30:
return "fast"
if minutes <= 45:
return "normal"
return "slow"
times = pd.Series([22, 41, 58, 30, 45])
print(times.apply(delivery_band).tolist())
print(times.apply(lambda m: f"{m // 60}h {m % 60}m").tolist())['fast', 'normal', 'slow', 'fast', 'normal']
['0h 22m', '0h 41m', '0h 58m', '0h 30m', '0h 45m']For a simple condition like this, the vectorised pd.cut or np.select (see Adding Columns) would be faster on big data; apply shines when the logic is genuinely complex.
DataFrame.apply: rows or columns#
On a DataFrame, apply passes whole columns by default (axis=0), or whole rows with axis=1:
import pandas as pd
scores = pd.DataFrame({"maths": [92, 67, 78], "science": [88, 74, 81], "english": [75, 90, 69]},
index=["Asha", "Ravi", "Meera"])
print(scores.apply(lambda col: col.max() - col.min())) # per subject
print(scores.apply(lambda row: row.idxmax(), axis=1)) # best subject per student
def summary(row):
return f"avg {row.mean():.1f}, best {row.max()}"
print(scores.apply(summary, axis=1))maths 25
science 14
english 21
dtype: int64
Asha maths
Ravi english
Meera science
dtype: object
Asha avg 85.0, best 92
Ravi avg 77.0, best 90
Meera avg 76.0, best 81
dtype: objectWith axis=1, each row is a Series whose labels are the column names.
Prefer vectorised code when you can#
Many apply calls have a faster vectorised equivalent:
Both give the same results:
import numpy as np
import pandas as pd
df = pd.DataFrame({"a": np.arange(5), "b": np.arange(5) * 10})
with_apply = df.apply(lambda r: r["a"] + r["b"], axis=1)
vectorised = df["a"] + df["b"]
print(with_apply.equals(vectorised), vectorised.tolist())True [0, 11, 22, 33, 44]On a DataFrame with a million rows, the row-wise apply is typically hundreds of times slower. Measure with %timeit in Jupyter if in doubt.
pipe: readable pipelines#
pipe passes the whole DataFrame to a function. It lets you chain your own cleaning steps like built-in methods:
import pandas as pd
def clean_names(df):
return df.assign(name=df["name"].str.strip().str.title())
def add_total(df, tax=0.18):
return df.assign(total=(df["price"] * (1 + tax)).round(2))
raw = pd.DataFrame({"name": [" pen ", "NOTEBOOK"], "price": [20, 60]})
result = raw.pipe(clean_names).pipe(add_total, tax=0.05)
print(result) name price total
0 Pen 20 21.0
1 Notebook 60 63.0Each step is a small, testable function, and the chain reads top to bottom.
Common mistakes#
Practice#
Practice exercise
Shipping costs depend on both weight and zone: zone "local" costs ₹40 plus ₹10 per kg; "national" costs ₹80 plus ₹20 per kg, and orders over 5 kg get ₹50 off. Write a function and apply it row by row to the orders weight=[1.0, 6.5, 3.0], zone=["local", "national", "national"].
Show hint
Write def shipping(row) that reads row["weight"] and row["zone"], then use df.apply(shipping, axis=1).
Show solution
import pandas as pd
def shipping(row):
if row["zone"] == "local":
cost = 40 + 10 * row["weight"]
else:
cost = 80 + 20 * row["weight"]
if row["weight"] > 5:
cost -= 50
return cost
orders = pd.DataFrame({"weight": [1.0, 6.5, 3.0], "zone": ["local", "national", "national"]})
orders["shipping"] = orders.apply(shipping, axis=1)
print(orders) weight zone shipping
0 1.0 local 50.0
1 6.5 national 160.0
2 3.0 national 140.0This rule could be vectorised with np.where, but the function version is clearer, and for a few thousand rows the speed difference does not matter.
Key takeaway#
Next: Grouping and Aggregating.
Finished this lesson?