kmail.at
contact

elsewhere

EnglishEN
← learning

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.

2026-09-01 · 5 min read

$ df.where()
0.10
introduced in pandas

December 2012 (verified in the v0.10.0 source)

6
parameters (pandas 3.0)

self, cond, other, inplace, axis, level — verified via inspect.signature

mask()
sibling method

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: float64

The -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: int64

Cap 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.90

Failed 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     NaN

Whole-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

  1. 1where(cond) keeps what passes and NaNs the rest; where(cond, other) swaps in a fallback — mask() is the same switch flipped
  2. 2Filtering that must change row count belongs to query(); where() is for fixing values in place
  3. 3Expect int → float upcast wherever NaN appears; follow with .fillna() or .astype() before exporting
  4. 4other= accepts an aligned DataFrame — patch failed sensors from a calibration table in one line
  5. 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 →

← all learning