By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.
A hyper-practical, zero-fluff guide for real projects and certifications
Outliers and skewness are the silent killers of data science models. They distort means, inflate variances, and turn linear regression into a guessing game. Ignoring them is like tuning a guitar with a broken string—no matter how hard you try, the music will sound wrong.
You’re building a credit risk model for a fintech startup. Your dataset has: - Outliers: A handful of ultra-high-net-worth individuals with $10M+ incomes (vs. the median $60K).- Skewness: 95% of loan amounts are <$50K, but the tail stretches to $500K.
If you don’t handle this:- Your logistic regression will treat the $10M earners as "normal," underestimating risk for everyone else.- The model’s AUC drops from 0.85 to 0.68 in production.- The business loses $2M in defaults before catching the error.
This guide gives you the tools to fix it—fast.
|z| > 3
Q1 - 1.5*IQR
Q3 + 1.5*IQR
0
>0
<0
|skewness| > 1
log(x)
log(0)
log1p(x)
log(1 + x)
RobustScaler
pandas
numpy
scipy
matplotlib
seaborn
scikit-learn
pip install pandas numpy scipy matplotlib seaborn scikit-learn
import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns from scipy import stats # Load synthetic loan data (replace with your dataset) url = "https://raw.githubusercontent.com/your-repo/loan_data.csv" df = pd.read_csv(url) # Check skewness and outliers print("Skewness:") print(df.skew().sort_values(ascending=False)) # Plot distributions for col in ["income", "loan_amount", "age"]: plt.figure(figsize=(10, 4)) sns.histplot(df[col], kde=True) plt.title(f"Distribution of {col}") plt.show()
Expected output:- income and loan_amount will show right skew (long right tail).- age might be left-skewed (few young borrowers).
income
loan_amount
age
def detect_outliers_iqr(data, column): Q1 = data[column].quantile(0.25) Q3 = data[column].quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - 1.5 * IQR upper_bound = Q3 + 1.5 * IQR outliers = data[(data[column] < lower_bound) | (data[column] > upper_bound)] return outliers outliers_income = detect_outliers_iqr(df, "income") print(f"Outliers in income: {len(outliers_income)}")
def detect_outliers_zscore(data, column, threshold=3): z_scores = np.abs(stats.zscore(data[column])) return data[z_scores > threshold] outliers_loan = detect_outliers_zscore(df, "loan_amount") print(f"Outliers in loan_amount: {len(outliers_loan)}")
Production insight:- IQR is safer for skewed data (z-score assumes normality).- Always visualize outliers before deciding to remove/transform them.
# Apply log transform (add 1 to avoid log(0)) df["income_log"] = np.log1p(df["income"]) df["loan_amount_log"] = np.log1p(df["loan_amount"]) # Check skewness after transform print("Skewness after log transform:") print(df[["income_log", "loan_amount_log"]].skew()) # Plot transformed distributions for col in ["income_log", "loan_amount_log"]: plt.figure(figsize=(10, 4)) sns.histplot(df[col], kde=True) plt.title(f"Log-transformed {col}") plt.show()
Expected output:- Skewness should drop significantly (e.g., from 5.2 to 0.8).- Distribution should look more normal.
5.2
0.8
⚠️ Trap:- Log transform only works for positive data. If your data has zeros/negatives: - Use log1p (log of 1 + x). - Or try Box-Cox (requires positive data) or Yeo-Johnson (works for any data).
log1p
1 + x
Box-Cox
Yeo-Johnson
from scipy.stats.mstats import winsorize # Winsorize at 5th and 95th percentiles df["income_winsorized"] = winsorize(df["income"], limits=[0.05, 0.05]) df["loan_amount_winsorized"] = winsorize(df["loan_amount"], limits=[0.05, 0.05]) # Plot winsorized data for col in ["income_winsorized", "loan_amount_winsorized"]: plt.figure(figsize=(10, 4)) sns.histplot(df[col], kde=True) plt.title(f"Winsorized {col}") plt.show()
Production insight:- Winsorizing is reversible (unlike trimming). You can always recover the original values.- Adjust the percentile limits based on your data (e.g., [0.01, 0.01] for stricter capping).
[0.01, 0.01]
# Compare original, log-transformed, and winsorized data comparison = pd.DataFrame({ "original_income": df["income"], "log_income": df["income_log"], "winsorized_income": df["income_winsorized"] }) print(comparison.describe())
Key metrics to compare:| Method | Mean | Std Dev | Skewness | Min | Max | |-----------------|-------|---------|----------|------|-------| | Original | 75K | 120K | 4.2 | 10K | 10M | | Log Transform | 10.5 | 1.2 | 0.3 | 9.2 | 16.1 | | Winsorized | 70K | 50K | 1.1 | 15K | 250K |
Which to choose?- Log transform: Best for modeling (reduces skewness, preserves relationships).- Winsorizing: Best for small datasets or when you can’t transform (e.g., categorical features).
from sklearn.preprocessing import RobustScaler # Scale winsorized data (less sensitive to outliers) scaler = RobustScaler() df[["income_scaled", "loan_amount_scaled"]] = scaler.fit_transform( df[["income_winsorized", "loan_amount_winsorized"]] ) print(df[["income_scaled", "loan_amount_scaled"]].describe())
Production insight:- Always scale after handling outliers/skewness. Scaling before can amplify the effect of outliers.- Use RobustScaler if you still have outliers after winsorizing.
df.describe()
DVC
Delta Lake
Evidently AI
Arize
df[df["income"] > 1e6]
ValueError: math domain error
NaN
StandardScaler
Answer: Log transform or winsorizing.
"What’s the risk of using z-score for outlier detection on skewed data?"
Answer: Z-score will flag too many "outliers" in skewed data. Use IQR instead.
"When should you use winsorizing over log transform?"
Answer: When you can’t transform (e.g., zeros/negatives) or need to preserve sample size.
"What’s the effect of log transform on a linear regression model?"
y = a * x^b
log(y) = log(a) + b*log(x)
You’re given a dataset of customer spending with a highly right-skewed spend_amount column. Your task: 1. Detect outliers using IQR.2. Apply log transform and winsorizing to spend_amount.3. Compare the skewness before/after each method.
spend_amount
Dataset:
import pandas as pd import numpy as np np.random.seed(42) spend = np.concatenate([ np.random.normal(100, 20, 1000), # Normal spenders np.random.normal(500, 50, 50), # High spenders np.random.normal(2000, 200, 5) # Outliers ]) df = pd.DataFrame({"spend_amount": spend})
# 1. Detect outliers with IQR Q1 = df["spend_amount"].quantile(0.25) Q3 = df["spend_amount"].quantile(0.75) IQR = Q3 - Q1 outliers = df[(df["spend_amount"] < Q1 - 1.5*IQR) | (df["spend_amount"] > Q3 + 1.5*IQR)] print(f"Outliers: {len(outliers)}") # 2. Apply log transform and winsorizing df["spend_log"] = np.log1p(df["spend_amount"]) df["spend_winsorized"] = winsorize(df["spend_amount"], limits=[0.05, 0.05]) # 3. Compare skewness print("Original skewness:", df["spend_amount"].skew()) print("Log skewness:", df["spend_log"].skew()) print("Winsorized skewness:", df["spend_winsorized"].skew())
Why it works:- IQR detects outliers without assuming normality.- Log transform reduces skewness by compressing large values.- Winsorizing caps extremes while preserving data points.
df.skew()
Q1 = df[col].quantile(0.25); Q3 = df[col].quantile(0.75); IQR = Q3 - Q1
< Q1 - 1.5*IQR
> Q3 + 1.5*IQR
np.abs(stats.zscore(df[col])) > 3
df[col + "_log"] = np.log1p(df[col])
df[col + "_winsorized"] = winsorize(df[col], limits=[0.05, 0.05])
limits
RobustScaler().fit_transform(df[[col]])
Join 4M+ learners. Unlock unlimited quizzes, wrong-answer tracking, flashcards + reminders, study guides, and 1-on-1 challenges.