Python for Data course menu
Find and Remove Duplicates and Outliers in pandas
Detect and remove duplicate rows with duplicated and drop_duplicates, keep the latest record per customer, and find outliers with the IQR rule and z-scores, then decide whether to remove, cap or keep them.
On this page
Two more problems turn up in almost every dataset: duplicate records (the same order loaded twice) and outliers (values far from the rest, such as an age of 250 or a ₹9,99,999 order). Both distort totals and averages if left alone.
Finding duplicates#
import pandas as pd
orders = pd.DataFrame({
"order_id": [101, 102, 102, 103, 104, 104],
"customer": ["Asha", "Ravi", "Ravi", "Meera", "John", "John"],
"amount": [250, 120, 120, 560, 90, 95],
})
print(orders.duplicated()) # entire row repeated?
print(orders.duplicated().sum(), "exact duplicate rows")
print(orders.duplicated(subset="order_id").sum(), "repeated order ids")
print(orders[orders.duplicated(subset="order_id", keep=False)])0 False
1 False
2 True
3 False
4 False
5 False
dtype: bool
1 exact duplicate rows
2 repeated order ids
order_id customer amount
1 102 Ravi 120
2 102 Ravi 120
4 104 John 90
5 104 John 95Order 102 is an exact copy. Order 104 appears twice with different amounts: a conflict that needs a decision, not just deletion. keep=False marks every copy, so you can see all of them.
Removing duplicates#
import pandas as pd
orders = pd.DataFrame({
"order_id": [101, 102, 102, 103, 104, 104],
"customer": ["Asha", "Ravi", "Ravi", "Meera", "John", "John"],
"amount": [250, 120, 120, 560, 90, 95],
})
print(orders.drop_duplicates()) # exact copies only
print(orders.drop_duplicates(subset="order_id", keep="last")) # one row per id, last wins order_id customer amount
0 101 Asha 250
1 102 Ravi 120
3 103 Meera 560
4 104 John 90
5 104 John 95
order_id customer amount
0 101 Asha 250
2 102 Ravi 120
3 103 Meera 560
5 104 John 95Keeping the latest record#
A common task: one row per customer, keeping their most recent update. Sort by date, then drop duplicates keeping the last:
import pandas as pd
updates = pd.DataFrame({
"customer": ["Asha", "Ravi", "Asha", "Ravi", "Meera"],
"updated": pd.to_datetime(["2026-01-05", "2026-01-07", "2026-02-10", "2026-01-02", "2026-03-01"]),
"plan": ["basic", "pro", "pro", "basic", "basic"],
})
latest = updates.sort_values("updated").drop_duplicates("customer", keep="last")
print(latest.sort_values("customer")) customer updated plan
2 Asha 2026-02-10 pro
4 Meera 2026-03-01 basic
1 Ravi 2026-01-07 proAsha moved to "pro" in February, so that row is kept.
Outliers with the IQR rule#
The interquartile range (IQR) is the spread of the middle half of the data: Q3 − Q1. A widely used rule flags values more than 1.5 × IQR below Q1 or above Q3. It does not depend on the mean, so a few extreme values do not hide themselves:
import pandas as pd
amounts = pd.Series([250, 310, 180, 270, 4200, 290, 230, 260, 15, 300])
q1, q3 = amounts.quantile([0.25, 0.75])
iqr = q3 - q1
low, high = q1 - 1.5 * iqr, q3 + 1.5 * iqr
print(q1, q3, iqr)
print(low, high)
print(amounts[(amounts < low) | (amounts > high)].tolist())235.0 297.5 62.5
141.25 391.25
[4200, 15]Outliers with z-scores#
A z-score says how many standard deviations a value is from the mean. Values beyond ±3 (sometimes ±2) are flagged:
import pandas as pd
amounts = pd.Series([250, 310, 180, 270, 4200, 290, 230, 260, 15, 300])
z = (amounts - amounts.mean()) / amounts.std()
print(z.round(2).tolist())
print(amounts[z.abs() > 2].tolist())[-0.3, -0.25, -0.36, -0.29, 2.84, -0.27, -0.32, -0.29, -0.49, -0.26]
[4200]The z-score method only caught 4200, and with a ±3 cut-off it would have caught nothing: the outlier itself inflates the standard deviation. That is why the IQR rule is often preferred for small or skewed data.
What to do with outliers#
- Check if it is an error. An age of 250 is a typo; fix or remove it.
- If it is real, keep it, but consider reporting the median instead of the mean, or analysing it separately (a bulk corporate order is real, but not typical).
- Cap extreme values to the limits (also called winsorising) when a model is sensitive to them:
import pandas as pd
amounts = pd.Series([250, 310, 180, 270, 4200, 290, 230, 260, 15, 300])
print(amounts.clip(lower=105, upper=435).tolist())
print(round(amounts.mean(), 1), amounts.median())[250, 310, 180, 270, 435, 290, 230, 260, 105, 300]
630.5 265.0One value pulls the mean far above the median; that gap is itself a sign of outliers.
Common mistakes#
Practice#
Practice exercise
Delivery times in minutes were [32, 28, 35, 30, 31, 29, 180, 33, 27, 34, 30]. Using the IQR rule, print the bounds and the outliers, then print the mean and median before and after removing the outliers (rounded to 1 decimal).
Show hint
Compute Q1 and Q3 with quantile, the bounds as Q1 − 1.5·IQR and Q3 + 1.5·IQR, then filter with between(low, high).
Show solution
import pandas as pd
times = pd.Series([32, 28, 35, 30, 31, 29, 180, 33, 27, 34, 30])
q1, q3 = times.quantile([0.25, 0.75])
low, high = q1 - 1.5 * (q3 - q1), q3 + 1.5 * (q3 - q1)
print(low, high)
print(times[~times.between(low, high)].tolist())
kept = times[times.between(low, high)]
print(round(times.mean(), 1), times.median())
print(round(kept.mean(), 1), kept.median())23.5 39.5
[180]
44.5 31.0
30.9 30.5The single 180-minute delivery raises the mean by more than 13 minutes but barely moves the median.
Key takeaway#
Finished this lesson?