pandas · difficulty ◆◆
pandas where() — keep or replace values by condition
where() keeps every row and quietly patches the bad cells — filtering's gentler sibling.
SQL's WHERE keeps rows. pandas' where() keeps values — same word, opposite instinct, and mixing them up is a rite of passage.
fully AI-generatedThis text is fully AI-generated and was published without human editorial review.$ df.where()- 0.10
- introduced in pandas
- 6
- parameters (pandas 3.0)
- mask()
- sibling method
December 2012 (verified in the v0.10.0 source)
self, cond, other, inplace, axis, level — verified via inspect.signature
df.mask(c) ≡ df.where(~c) — one-line inverse in the source
What it does
df.where(cond) returns a frame of the same shape where cells satisfying cond keep their value and everything else becomes NaN. Pass other= and the losing cells get that value instead — a scalar, a Series, a similarly-shaped DataFrame, even a callable. The boolean twin mask() does the exact opposite: df.mask(cond) wipes the cells where cond is True. Together they cover 'blank the bad data' and 'replace the bad data' without touching row count.
Why it matters
Real data arrives dirty: sensor glitches read as -1, capped meters top out, a probe fails and needs a calibration value instead. Boolean indexing and query() answer 'which rows do I keep?' — but fixing values means keeping the row and patching the cell. That is where()'s whole job, and it does it with label alignment: hand it a lookup table keyed by column name and it patches the right cells from the right columns. np.where() cannot do that; it collapses everything to a bare array and forgets the index.
Example
$ df = pd.DataFrame({"city": ["Berlin", "Paris", "Rome", "Oslo"], "pm25": [24, -1, 31, 19]})
df.pm25.where(df.pm25 >= 0)0 24.0
1 NaN
2 31.0
3 19.0
Name: pm25, dtype: float64The -1 glitch becomes NaN; every row survives for later imputation. Note the upcast to float64 — NaN forces it.
$ sales = pd.Series([120, 87, 300, 95])
sales.where(sales <= 100, 100)0 100
1 87
2 100
3 95
dtype: int64Cap at a ceiling with other= — no NaN, no dtype change, the report stays honest at 100.
$ measured = pd.DataFrame({"sensor_a": [0.98, 0.11], "sensor_b": [0.75, 0.40]})
calibrated = pd.DataFrame({"sensor_a": [1.00, 1.00], "sensor_b": [0.90, 0.90]})
measured.where(measured > 0.5, calibrated) sensor_a sensor_b
0 0.98 0.75
1 1.00 0.90Failed sensor_a readings (0.11) get replaced from the calibration table; sensor_b's 0.40 too. Alignment is by label, per column.
$ df2 = pd.DataFrame({"city": ["Berlin", "Paris", "Rome"], "pm25": [24, 19, 31], "year": [2024, 2024, 2025]})
df2.where((df2.pm25 < 30) & (df2.year == 2024)) city pm25 year
0 Berlin 24.0 2024.0
1 Paris 19.0 2024.0
2 NaN NaN NaNWhole-frame where: one condition per row blanks entire failing rows — strings included, 'Rome' becomes NaN.
Common flags
- df.where(cond)
- keep cells where cond is True, everything else becomes NaN
- df.where(cond, other)
- other: scalar, Series, same-shape DataFrame, or callable — the replacement values
- df.mask(cond, other)
- the inverted twin: replace where cond is True (mask(c) ≡ where(~c))
- cond as callable
- df.where(lambda d: d.pm25 > 20) — the condition is computed on the whole frame
- axis=0|1
- alignment axis when other is a Series — axis=0 aligns per column, axis=1 per row
- inplace=True
- mutate the frame instead of returning a copy (defaults to False; discouraged)
History
Arrived with pandas 0.10 (December 17, 2012)
DataFrame.where() and mask() first appear in the 0.10.0 source — v0.7.3 (April 2012) has neither. The original signature was bare: where(self, cond, other=NA, inplace=False), implemented as a thin wrapper over np.where(cond, self, other), with np.putmask doing the in-place variant. mask() had no other parameter at all; it was literally one line: return self.where(~cond, NA).
Maturing into the generic NDFrame API
By 0.13 the method had moved onto the shared NDFrame base so Series and DataFrame behave identically, gaining axis, level, and callable cond/other along the way. In 1.x, inplace=True returned None — a classic trap — but modern pandas (3.0) returns self, so df = df.where(...) no longer nukes your frame.
Under the hood
Under the hood: block dispatch to NumPy
where() does not loop over cells. The condition and other are aligned (index and columns), then pandas splits the frame into dtype blocks (float, int, object…) and calls the block-level where in pandas/core/internals/blocks.py, which applies NumPy's where elementwise per block and consolidates the result. The public expression-level helper lives in pandas/core/computation/expressions.py — the same numexpr plumbing that eval() and query() use. Mixed dtypes mean multiple block calls; that is also why an int column silently upcasts to float when NaN enters.
Fun facts
Pros & cons
pros
- + Keeps shape and labels — patch values without losing rows or the index
- + other= accepts scalars, aligned Series/DataFrames, or callables, so lookups and computed fallbacks are one call
- + Pairs with fillna/clip into clean fix-up chains instead of nested np.where ternaries
cons
- − A False condition wipes string cells to NaN — whole-row conditions blank text columns you never intended to touch
- − NaN propagation upcasts integer columns to float, which surprises anyone building IDs or counts
- − The where-keeps / mask-replaces inversion reads backwards until it clicks — and the SQL word means something else entirely
Takeaways
- 1where(cond) keeps what passes and NaNs the rest; where(cond, other) swaps in a fallback — mask() is the same switch flipped
- 2Filtering that must change row count belongs to query(); where() is for fixing values in place
- 3Expect int → float upcast wherever NaN appears; follow with .fillna() or .astype() before exporting
- 4other= accepts an aligned DataFrame — patch failed sensors from a calibration table in one line
- 5Only capping extremes? clip() does it with one argument and no NaN drama
Related commands
archive entry — written and published by a scheduled job, never editorially reviewed. Kept for reference: out of the site’s search index, and no ads run here. the reviewed library →
