Use mask() to replace values where a condition is True. Choose whether to change one column, selected cells or whole rows, and keep invalid source values available for review.
mask(cond, other) replaces values where cond is True and preserves them where it is False. It keeps the shape; it does not filter rows. For row selection, use loc. Use where() when the condition describes values to keep. Current arguments include cond, other, inplace, axis and level; omit obsolete errors and try_cast arguments.
Start with the student table. Assign the returned Series back to MATH to change only that column. The score 80 is preserved because the comparison is strictly greater than 80.
import pandas as pd
def student_data():
return pd.DataFrame({"NAME": ["Ravi", "Raju", "Alex", "Ron", "King", "Jack"], "ID": [1, 2, 3, 4, 5, 6], "MATH": [80, 40, 70, 70, 82, 30], "ENGLISH": [81, 70, 40, 50, 60, 30]})
df = student_data()
df["MATH"] = df["MATH"].mask(df["MATH"].gt(80), -5)
print(df)
assert df["MATH"].tolist() == [80, 40, 70, 70, -5, 30]
assert df.loc[4, "NAME"] == "King"Expected output
NAME ID MATH ENGLISH
0 Ravi 1 80 81
1 Raju 2 40 70
2 Alex 3 70 40
3 Ron 4 70 50
4 King 5 -5 60
5 Jack 6 30 30A row condition can affect every column. Use a compatible replacement for each column so a string column does not receive a numeric value. Replacing a whole row also changes its ID, so choose this deliberately. For scores alone, target a Series or numeric subset.
df = student_data()
replacement = pd.DataFrame({"NAME": ["review"] * len(df), "ID": [-5] * len(df), "MATH": [-5] * len(df), "ENGLISH": [-5] * len(df)}, index=df.index)
result = df.mask(df["MATH"].gt(80), replacement)
print(result)
assert result.loc[4].tolist() == ["review", -5, -5, -5]
assert df.loc[4, "NAME"] == "King"Expected output
NAME ID MATH ENGLISH
0 Ravi 1 80 81
1 Raju 2 40 70
2 Alex 3 70 40
3 Ron 4 70 50
4 review -5 -5 -5
5 Jack 6 30 30Use & for AND and | for OR, with parentheses around comparisons. This fresh table gives Alex a score of 73. Assign the result back to the column; do not rely on df["MATH"].mask(..., inplace=True) to update its parent DataFrame.
df = student_data()
df.loc[2, "MATH"] = 73
condition = (df["MATH"] > 70) & (df["MATH"] < 75)
df["MATH"] = df["MATH"].mask(condition, -7)
print(df)
assert df["MATH"].tolist() == [80, 40, -7, 70, 82, 30]Expected output
NAME ID MATH ENGLISH
0 Ravi 1 80 81
1 Raju 2 40 70
2 Alex 3 -7 40
3 Ron 4 70 50
4 King 5 82 60
5 Jack 6 30 30Omitting other introduces a missing value. A nullable integer column keeps integer values and uses pd.NA. Ordinary NumPy integer columns may become floating point when a missing value is introduced. A missing score should not automatically become zero.
scores = pd.Series([80, -1, 105, 70], dtype="Int64", name="score")
invalid = scores.lt(0) | scores.gt(100)
cleaned = scores.mask(invalid)
print(cleaned)
print("dtype:", cleaned.dtype)
assert str(cleaned.dtype) == "Int64"
assert cleaned.isna().tolist() == [False, True, True, False]Expected output
0 80
1 <NA>
2 <NA>
3 70
Name: score, dtype: Int64
dtype: Int64For a complete Boolean condition, mask(invalid) and where(~invalid) produce the same result. Define whether the condition means replace or keep before choosing a method.
values = pd.Series([10, -2, 0, 8], dtype="Int64")
invalid = values.lt(0)
masked = values.mask(invalid, 0)
kept = values.where(~invalid, 0)
print(masked)
assert masked.equals(kept)
assert masked.tolist() == [10, 0, 0, 8]Expected output
0 10
1 0
2 0
3 8
dtype: Int64A replacement Series aligns by index labels, not its displayed order. axis="index" makes row alignment explicit for a DataFrame. Provide replacements for every affected label; missing replacement labels can introduce missing values.
df = pd.DataFrame({"amount": [10, -2, -3]}, index=["A", "B", "C"])
replacement = pd.Series([300, 200, 100], index=["C", "B", "A"])
result = df.mask(df.lt(0), replacement, axis="index")
print(result)
assert result["amount"].tolist() == [10, 200, 300]Expected output
amount
A 10
B 200
C 300A callable receives the object being masked. Keep it free of side effects. If the replacement depends on the data, other can also be a callable.
values = pd.Series([1, 20, 3], dtype="Int64")
result = values.mask(lambda s: s.gt(10), lambda s: s.clip(upper=10))
print(result)
assert result.tolist() == [1, 10, 3]Expected output
0 1
1 10
2 3
dtype: Int64inplace=True changes the object on which mask is called. Use a standalone Series for this demonstration and inspect that object after the call. For a DataFrame column, explicit assignment is clearer and works with Pandas 3 Copy-on-Write.
scores = pd.Series([80, 40, 70, 70, 82, 30], name="MATH")
condition = scores.gt(80)
scores.mask(condition, -5, inplace=True)
print(scores)
assert scores.iloc[4] == -5Expected output
0 80
1 40
2 70
3 70
4 -5
5 30
Name: MATH, dtype: int64The rule for this example is a non-negative numeric amount and a non-blank product. Preserve raw values, turn invalid amounts into missing values, and retain rejected rows for inspection. Trimming blank products uses string inspection; missing-value checks identify review rows.
raw = pd.DataFrame({"order_id": [101, 102, 103, 104, 105], "product": [" Pen ", "Book", "Notebook", " ", "Pencil"], "amount": ["5.50", "-3", "bad", "8", "0"]})
cleaned = raw.copy()
amount = pd.to_numeric(raw["amount"], errors="coerce").astype("Float64")
# A missing comparison is not evidence of a negative amount.
negative = amount.lt(0).fillna(False)
cleaned["amount"] = amount.mask(negative)
product = raw["product"].astype("string").str.strip()
cleaned["product"] = product.mask(product.eq(""))
needs_review = cleaned[["product", "amount"]].isna().any(axis=1)
accepted = cleaned.loc[~needs_review].copy()
review = raw.loc[needs_review].copy()
print("Accepted:")
print(accepted)
print("Review original values:")
print(review)
assert accepted["order_id"].tolist() == [101, 105]
assert review["order_id"].tolist() == [102, 103, 104]
assert accepted["amount"].sum() == 5.5
assert raw.loc[1, "amount"] == "-3"Expected output
Accepted:
order_id product amount
0 101 Pen 5.5
4 105 Pencil 0.0
Review original values:
order_id product amount
1 102 Book -3
2 103 Notebook bad
3 104 8Reason counts can overlap; use the combined row mask for the total number needing review. A zero amount is allowed by this sample rule. For export options, follow to_excel() and Input and Output.
print("Negative amount:", int(negative.sum()))
print("Unparseable or missing amount:", int(amount.isna().sum()))
print("Blank or missing product:", int(cleaned["product"].isna().sum()))
print("Review rows:", int(needs_review.sum()))
print(accepted.to_csv(index=False).rstrip())
# Optional Colab export:
# accepted.to_csv("accepted_sales.csv", index=False)Expected output
Negative amount: 1
Unparseable or missing amount: 1
Blank or missing product: 1
Review rows: 3
order_id,product,amount
101,Pen,5.5
105,Pencil,0.0Does mask() remove rows? No; it replaces values. Why did every column change? A row condition was applied to the whole DataFrame. Can I replace text with a number? The replacement must be compatible with the target dtype; prefer a text marker or missing value for text. How should missing conditions behave? Set an explicit policy, such as fillna(False), when unknown should mean do not replace. Why did inplace on a selected column not update the table? Assign the returned Series back to the DataFrame. When should I use loc? Use it to set particular columns or select rows. Should invalid amounts become zero? Only if your data rules establish that they represent zero; otherwise keep them missing for review.
For temperatures 18, -99, 22 and 85, define valid readings as -20 through 60 inclusive. Keep the raw Series, replace invalid readings with missing values and confirm that two readings remain.
The nullable Float64 dtype supports missing readings while preserving numeric values.
raw_temperature = pd.Series([18, -99, 22, 85], dtype="Float64")
invalid = raw_temperature.lt(-20) | raw_temperature.gt(60)
answer = raw_temperature.mask(invalid)
print(answer)
print("Valid readings:", answer.count())
assert answer.count() == 2
assert answer.dropna().tolist() == [18, 22]
assert raw_temperature.tolist() == [18, -99, 22, 85]Expected output
0 18.0
1 <NA>
2 22.0
3 <NA>
dtype: Float64
Valid readings: 2Open in Google Colab View on GitHub
Run the examples in order, change the replacement rules and inspect the cleaned values and review rows. Save your own copy to keep edits. All sample tables are included in the notebook.
Continue with the Data Cleaning hub, where(), loc, at, NumPy where(), fillna() and iloc.
Reference: Pandas DataFrame.mask documentation.
Author & Instructor at plus2net
I write and maintain practical tutorials on Python, PHP, SQL, JavaScript, HTML, jQuery, and web development at plus2net. The tutorials focus on clear explanations, working examples, and code that readers can test and adapt while learning.