Conditional Columns
Create columns from conditions the vectorised way: np.where for two outcomes, np.select for many, Series.where/mask to keep or replace values, and loc for in-place updates.
"If a batter scored 400+ runs he is In form, otherwise Out of form." "Tag players as Top Batter, Top Bowler, All-rounder or Squad Player." Creating a column from a condition is one of the most common pandas jobs. You already know apply with an if/else function — this topic shows the vectorised tools that do the same job far faster and in one line.
text
player runs wickets strike_rate
0 Virat 741 0 154.4
1 Rohit 417 0 139.0
2 Dhoni 161 0 146.4
3 Gill 890 0 158.9
4 Bumrah 12 20 80.0
5 Jadeja 280 8 147.4Definition
A conditional column is a column whose value in each row is chosen according to a boolean condition evaluated on that row. In pandas you build it vectorised: compute the condition for the whole column at once (a boolean Series), then pick values with
np.where,np.select,Series.where/mask, or.loc.
np.where(cond, a, b)— two outcomes:awherecondis True,belsewhere.np.select(condlist, choicelist, default)— many outcomes; the first True condition wins.s.where(cond, other)— keep values wherecondis True, replace the rest withother(NaN by default).s.mask(cond, other)— the opposite: replace values wherecondis True.
Syntax
python
np.where(condition, value_if_true, value_if_false)
np.select(condlist, choicelist, default=0)
series.where(cond, other=nan) # keep where True
series.mask(cond, other=nan) # replace where True
df.loc[condition, "col"] = value # update matching rows in place| Function / parameter | Meaning | Default |
|---|---|---|
condition / cond | boolean Series/array, same length as the data | required |
value_if_true, value_if_false | scalars or arrays/Series of the same length | required |
condlist | list of boolean Series, checked in order | required |
choicelist | list of values, one per condition | required |
default | value when no condition is True | 0 |
other (where/mask) | replacement value (scalar, Series or callable) | NaN |
Note: combine conditions with
&(and),|(or),~(not), and wrap each one in brackets:(df["runs"] >= 200) & (df["wickets"] >= 5). Python'sand/ordo not work on Series.
Example — two outcomes with np.where
python
import pandas as pd
import numpy as np
df = pd.DataFrame({
"player": ["Virat", "Rohit", "Dhoni", "Gill", "Bumrah", "Jadeja"],
"runs": [741, 417, 161, 890, 12, 280],
"wickets": [0, 0, 0, 0, 20, 8],
})
df["form"] = np.where(df["runs"] >= 400, "In form", "Out of form")
print(df)text
player runs wickets form
0 Virat 741 0 In form
1 Rohit 417 0 In form
2 Dhoni 161 0 Out of form
3 Gill 890 0 In form
4 Bumrah 12 20 Out of form
5 Jadeja 280 8 Out of formHow it works: df["runs"] >= 400 gives 6 True/False values in one go. np.where then picks "In form" for each True and "Out of form" for each False — no Python loop. The two outcomes can also be columns: np.where(df["runs"] >= 400, df["runs"], 0) gives [741 417 0 890 0 0].
Nesting np.where gives three outcomes, but it gets hard to read quickly:
python
df["level"] = np.where(df["runs"] >= 700, "Orange Cap race",
np.where(df["runs"] >= 250, "Solid", "Low"))
print(df[["player", "runs", "level"]])text
player runs level
0 Virat 741 Orange Cap race
1 Rohit 417 Solid
2 Dhoni 161 Low
3 Gill 890 Orange Cap race
4 Bumrah 12 Low
5 Jadeja 280 SolidExample — many outcomes with np.select
python
conditions = [
df["runs"] >= 500,
df["wickets"] >= 15,
(df["runs"] >= 200) & (df["wickets"] >= 5),
]
choices = ["Top Batter", "Top Bowler", "All-rounder"]
df["role_tag"] = np.select(conditions, choices, default="Squad Player")
print(df[["player", "runs", "wickets", "role_tag"]])text
player runs wickets role_tag
0 Virat 741 0 Top Batter
1 Rohit 417 0 Squad Player
2 Dhoni 161 0 Squad Player
3 Gill 890 0 Top Batter
4 Bumrah 12 20 Top Bowler
5 Jadeja 280 8 All-rounderHow it works: for each row NumPy checks the conditions in order and uses the choice of the first one that is True; if none is True it uses default. Order matters — put the most specific condition first:
python
print(np.select([df["runs"] >= 100, df["runs"] >= 500], ["100+", "500+"], default="<100"))text
['100+' '100+' '100+' '100+' '<100' '100+']Virat (741) got "100+" because the 100+ check came first and matched — the 500+ label is never reached.
Example — keep or replace values with where / mask
python
r = df.set_index("player")["runs"]
print(r.where(r >= 200)) # keep 200+, others -> NaN
print(r.mask(r < 200, 0)) # replace under-200 with 0text
player
Virat 741.0
Rohit 417.0
Dhoni NaN
Gill 890.0
Bumrah NaN
Jadeja 280.0
Name: runs, dtype: float64
player
Virat 741
Rohit 417
Dhoni 0
Gill 890
Bumrah 0
Jadeja 280
Name: runs, dtype: int64How it works: where keeps values where the condition is True. The replaced values became NaN, which forced the dtype to float64 (NaN is a float). mask is the mirror image — it replaces where the condition is True. r.where(r >= 200, 0) and r.mask(r < 200, 0) give the same result.
Note:
pandas.Series.whereandnp.whereare different!np.where(cond, a, b)chooses between two values;s.where(cond, other)keeps the Series' own values and only replaces the False positions.
Example — update matching rows with loc
python
d = df[["player", "runs"]].copy()
d["badge"] = "-"
d.loc[d["runs"] >= 400, "badge"] = "Gold"
print(d)text
player runs badge
0 Virat 741 Gold
1 Rohit 417 Gold
2 Dhoni 161 -
3 Gill 890 Gold
4 Bumrah 12 -
5 Jadeja 280 -.loc[mask, "col"] = value changes only the matching rows. In pandas 3 this is the correct way to update part of a column — chained forms like d["badge"][mask] = "Gold" never modify d because of Copy-on-Write.
Tip: for numeric ranges (0–250 Low, 250–500 Medium, 500+ High)
pd.cut(df["runs"], bins=[0, 250, 500, 1000], labels=["Low", "Medium", "High"])is the cleanest choice — it returns an orderedcategorycolumn.
Comparison — which tool?
| Need | Best tool |
|---|---|
| 2 outcomes | np.where(cond, a, b) |
| 3+ outcomes, different conditions | np.select(conds, choices, default) |
| numeric ranges → labels | pd.cut |
| exact value look-up (code → name) | Series.map(dict) |
| keep good values, blank/replace others | Series.where / Series.mask / clip |
| change some rows of an existing column | df.loc[cond, "col"] = value |
| truly complex logic (loops, strings, several steps) | apply(func, axis=1) — slowest |
Common mistakes
- ❌
if df["runs"] > 400:→ValueError: The truth value of a Series is ambiguous. ✅ Usenp.whereor a mask — a Series holds many True/False values, not one. - ❌
df["runs"] > 200 and df["wickets"] > 5✅(df["runs"] > 200) & (df["wickets"] > 5). - ❌ Putting a general condition before a specific one in
np.select. ✅ First match wins — order from most specific to least. - ❌
np.select(conds, choices)with text choices and the default0. ✅ Always give a textdefault=so the column isn't a mix of strings and 0. - ❌
df["col"][mask] = value(chained assignment — silently does nothing in pandas 3). ✅df.loc[mask, "col"] = value.
Interview tip
Q: How would you replace a row-wise apply with if/elif/else to make it faster?
A: Build each condition as a boolean Series and use np.select(conditions, choices, default=...) (or pd.cut for numeric ranges). It evaluates all rows at once in compiled code instead of calling a Python function per row.
Key points to remember
- Build conditions for the whole column at once (boolean Series) instead of looping row by row.
- np.where(cond, a, b) gives two outcomes; a and b can be scalars or whole columns.
- np.select(conditions, choices, default=...) handles many outcomes — the FIRST True condition wins.
- Series.where keeps values where the condition is True; Series.mask replaces them. Replaced values default to NaN.
- Combine conditions with &, |, ~ and wrap each in brackets — not and/or.
- Use df.loc[cond, 'col'] = value to update matching rows; chained assignment never works in pandas 3.
- For numeric ranges use pd.cut; for code look-ups use map; keep apply(axis=1) for truly complex logic.
Try it yourself
np.where and np.select
where, mask, clip and loc
Practice: bowling economy labels
Revision Flashcards
Tap a card to flip it and see the answer.
