EDA and Feature Engineeringpandas 3.0 · scikit-learn 1.9 · seaborn 0.13 · Python 3.12+
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
72 small wins to finish your pathNext lesson →

Treating outliers by capping or dropping

Capping replaces every value past a boundary with the boundary itself, which keeps the row, keeps its other columns and removes the pull the far value had.

Last updated: 12 Oct, 2026 · pandas 3.0 · scikit-learn 1.9

The IQR fence produced a number: a fence at 65.63 with 116 fares past it. This lesson is about what to do with those 116 rows, and about why the answer is almost never to delete them.

Syntax: Series.clip pulls every value back inside a boundary in one call.

python
capped = s.clip(upper=65.63)          # anything above becomes 65.63
capped = s.clip(lower=0, upper=65.63)  # both ends at once

Capping the Fare column at its fence

The boundary comes from the fence

The cap is not a round number picked by hand. It is the fence the previous lesson computed, so the same rule that flagged the rows also decides where they land.

python
q1, q3 = fare.quantile(0.25), fare.quantile(0.75)
fence = q3 + 1.5 * (q3 - q1)

Writing the capped column back

clip returns a new Series, so the result has to be assigned. Under pandas 3 a write through df['Fare'][mask] = fence lands on a throwaway copy and changes nothing, which Copy-on-write and chained assignment shows in full.

python
df["Fare"] = df["Fare"].clip(upper=fence)   # assign it back

What capping changed, and what it did not

ExampleTitanic Fare before and after capping at the fence
import matplotlib
matplotlib.use("svg")
import matplotlib.pyplot as plt
import pandas as pd

URL = ("https://raw.githubusercontent.com/krishnaik06/Feature-Engineering-Live-sessions/"
       "master/titanic.csv")
fare = pd.read_csv(URL)["Fare"]

q1, q3 = fare.quantile(0.25), fare.quantile(0.75)
fence = q3 + 1.5 * (q3 - q1)
capped = fare.clip(upper=fence)

print("fence:", fence)
print("values changed:", int((fare > fence).sum()), "of", len(fare))
print("rows lost:", len(fare) - len(capped))
print("max   before:", fare.max(), " after:", capped.max())
print("mean  before:", round(fare.mean(), 4), " after:", round(capped.mean(), 4))
print("median before:", fare.median(), " after:", capped.median())

fig, ax = plt.subplots(figsize=(7, 2.6))
ax.boxplot([fare, capped], orientation="horizontal", widths=0.5)
ax.set_yticklabels(["before", "after"])
ax.set_xlabel("Fare")
fig.savefig("chart.svg")
Two horizontal box plots of the Titanic Fare column. The lower one, labelled before, has a long line of circles running out to 512. The upper one, labelled after, has the same box and a whisker that stops at the fence, with no circles at all, because every value past the fence now sits at the fence.

Reading the before and after

  • 116 values changed and no rows were lost. Every passenger is still in the frame, with their class, age and port untouched. That is the whole argument for capping over dropping.
  • The maximum fell from 512.33 to 65.63. The column now stops at the fence by construction.
  • The mean fell from 32.20 to 24.05 and the median did not move. The median was never affected by the tail, so capping could not change it.
  • The box plot has no points outside the whiskers. It cannot have any: every value past the fence is now exactly at the fence, and the fence was recomputed on a column whose tail is gone.

The last point is worth holding on to. A capped column always looks clean in a box plot. The chart stops being evidence once you have capped, so record what you capped and at what value rather than reading it back off the picture.

Measuring capping against dropping

Three models on the same two columns and the same test rows: one left alone, one with Fare capped, one with the flagged training rows deleted. The fence is computed from the training half only, which is why it comes out at 66.58 here and not at the 65.63 the whole column gave. A boundary computed from the test rows has read data the model will not have, which Data leakage covers.

ExampleFare left alone, capped, and dropped, on the same test rows
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split

URL = ("https://raw.githubusercontent.com/krishnaik06/Feature-Engineering-Live-sessions/"
       "master/titanic.csv")
df = pd.read_csv(URL, usecols=["Age", "Fare", "Survived"])
df["Age"] = df["Age"].fillna(df["Age"].median())
X_train, X_test, y_train, y_test = train_test_split(
    df[["Age", "Fare"]], df["Survived"], test_size=0.3, random_state=0)

def auc(a, b, c, d, label):
    model = LogisticRegression(max_iter=1000).fit(a, b)
    print(f"{label:22s} roc_auc {roc_auc_score(d, model.predict_proba(c)[:, 1]):.4f}"
          f"   train rows {len(a)}")

q1, q3 = X_train["Fare"].quantile(0.25), X_train["Fare"].quantile(0.75)
fence = q3 + 1.5 * (q3 - q1)              # from the training half only
print("fence:", round(fence, 4))
auc(X_train, y_train, X_test, y_test, "left as they are")
auc(X_train.assign(Fare=X_train["Fare"].clip(upper=fence)), y_train,
    X_test.assign(Fare=X_test["Fare"].clip(upper=fence)), y_test, "Fare capped")
keep = X_train["Fare"] <= fence
auc(X_train[keep], y_train[keep], X_test, y_test, "outlier rows dropped")
print("rows dropped:", int((~keep).sum()), "of", len(X_train))

What the three scores say

  • Both treatments beat leaving the column alone. 0.7263 goes to 0.7413 with capping and 0.7425 with dropping. Logistic regression is one of the models the far values were pulling.
  • Capping and dropping score within 0.0012 of each other. On this column there is no accuracy argument for deleting rows.
  • Dropping cost 84 of 623 training rows. 13% of the data, and every other column in those rows went with it.
  • Only one of the two survives prediction time. A capped column can be capped again on new data. A dropped row cannot be dropped from a request the model has been asked to answer, so the model meets rows at prediction time of a kind it never saw in training.

Cap, drop or keep

What it doesCostUse it when
Cappulls the value back to a boundarythe magnitude beyond the boundary is lostthe value is real but its size is distorting the fit
Drop the rowremoves the rowthe whole row goes, and it cannot be done at prediction timethe row is a confirmed error you cannot correct
Treat as missingreplaces the value with NaN, then imputesan invented value takes the place of a real onethe value is impossible, such as an age of 300
Keep itchanges nothingthe model has to tolerate the tailthe model is a tree, or the tail is the thing you are predicting
Transform the columna log or similar shrinks the tailevery value changes, and predictions come back in new unitsthe column is skewed rather than dirty

Fraud, failure and churn all live in the tail. In those problems the rows the fence flags are the rows the model exists to find, and the right answer is the last two rows of that table.

Where each choice shows up

  • Capping a price column before a linear model. The handful of enormous orders stop setting the slope, and every customer stays in the training set.
  • Treating an impossible value as missing. A birth year of 1800 is not a cap candidate; it is a hole, and the missing-value part already has the tools for it.
  • Keeping everything and changing model. A gradient boosting model splits on order, so the 512 fare and the 100 fare fall on the same side of every split above 100 and the tail costs nothing.
Watch out. Compute the boundary on the training rows and apply it to both halves. A fence computed on the whole dataset has read the test rows, and the score it produces is better than the score the same code will produce in production.
Try it yourself
  • Cap at the extreme fence, q3 + 3 * (q3 - q1), and read the three scores again.
  • Add lower= to the clip call with the lower fence and check that nothing changes, because the minimum fare is 0.
  • Drop the rows from the test half as well as the training half and watch the score rise for a reason that will not hold in production.
PreviousThe IQR fence

Every expert started right here.