Valuation | Volatility | IQR와 MAD를 활용한 이상치 제거 방법 | Python | R
Aug 18, 2025
Aug 18, 2025
Aug 18, 2025
데이터 전처리 과정에서 이상치(outlier)를 적절히 제거하지 않으면 분석 결과가 왜곡될 수 있다.
대표적인 이상치 탐지 방법은 IQR(Interquartile Range) 과 MAD(Median Absolute Deviation) 이다.
IQR 방법 (사분위수 범위)
정의
- IQR = Q3 − Q1
(Q1: 제1사분위수, Q3: 제3사분위수)
이상치 기준
-
하한 = Q1 − k × IQR
-
상한 = Q3 + k × IQR
-
일반적으로 k=1.5 → mild outlier, k=3 → extreme outlier
장단점
- 장점: 계산이 간단하고 Boxplot과 직관적으로 연결됨
- 단점: 분포가 심하게 비대칭(skewed)인 경우 한계가 있음
파이썬 예시
import numpy as np
data = np.array([10, 12, 12, 13, 12, 11, 200])
Q1, Q3 = np.percentile(data, [25, 75])
IQR = Q3 - Q1
lower, upper = Q1 - 1.5 * IQR, Q3 + 1.5 * IQR
outliers = data[(data < lower) | (data > upper)]
print("IQR 기준 이상치:", outliers)
MAD 방법 (중위 절대 편차)
정의
- median(|Xi − median(X)|)
여기에 보정계수 1.4826을 곱하면 정규분포에서 표준편차와 유사하게 사용 가능
$$ \text{MAD} = 1.4826 \times \text{median} (\left\vert Xi − \text{median}(X) \right\vert ) $$
이상치 기준
- Modified Z-score
$$ Z = 0.6745 \times \frac{(Xi − median)}{MAD} $$
- 일반적으로 |Z| > 2.5 → 이상치로 판정
장단점
- 장점: 극단값의 영향을 거의 받지 않아 강건(robust)
- 단점: 직관성이 다소 떨어짐
파이썬 예시
import numpy as np
data = np.array([10, 12, 12, 13, 12, 11, 200])
median = np.median(data)
mad = np.median(np.abs(data - median))
modified_z_scores = 0.6745 * (data - median) / mad
outliers = data[np.abs(modified_z_scores) > 2.5]
print("MAD 기준 이상치:", outliers)
IQR & MAD 이상치 제거: 파이썬 / R 실전 코드
아래 코드는 이상치 제거 함수 모음이다.
이상치 제거 (단일 열: 로그 수익률 가정) — Python & R
- 입력 데이터는 하나의 열(예:
log_return
)만 있다고 가정한다. - 두 방법(IQR, MAD) 모두 이상치 인덱스와 제거된 시리즈를 반환한다.
NaN
은 이상치로 보지 않으며, 그대로 유지(필요 시 사전 처리).
파이썬(Python)
import numpy as np
import pandas as pd
# --- IQR 기반 (Q1,Q3 → IQR → 하한/상한) -----------------------------
def iqr_clean_series(s: pd.Series, k: float = 1.5):
"""
s: 단일 수치형 Series (예: 로그 수익률)
k: 1.5(mild), 3(extreme)
반환: cleaned(이상치 제거된 Series, 인덱스 reset 안 함), outlier_idx(Int64Index), bounds(dict)
"""
s = pd.to_numeric(s, errors="coerce")
q1 = s.quantile(0.25)
q3 = s.quantile(0.75)
iqr = q3 - q1
lower = q1 - k * iqr
upper = q3 + k * iqr
mask_out = (s < lower) | (s > upper)
mask_out = mask_out & s.notna() # NaN은 제외
cleaned = s.loc[~mask_out]
out_idx = s.index[mask_out]
bounds = {"Q1": q1, "Q3": q3, "IQR": iqr, "lower": lower, "upper": upper}
return cleaned, out_idx, bounds
# --- MAD 기반 (Modified Z-score) ------------------------------------
def mad_clean_series(s: pd.Series, threshold: float = 2.5):
"""
s: 단일 수치형 Series (예: 로그 수익률)
threshold: |modified z| > threshold 이면 이상치
반환: cleaned(이상치 제거된 Series, 인덱스 reset 안 함), outlier_idx(Int64Index), stats(dict)
"""
s = pd.to_numeric(s, errors="coerce")
med = s.median(skipna=True)
mad_raw = (s - med).abs().median(skipna=True)
if pd.isna(mad_raw) or mad_raw == 0:
# 변동이 거의 없거나 모두 동일값 → 이상치 없음 처리
mask_out = pd.Series(False, index=s.index)
z = pd.Series(0.0, index=s.index)
else:
z = 0.6745 * (s - med) / mad_raw
mask_out = z.abs() > threshold
mask_out = mask_out & s.notna()
cleaned = s.loc[~mask_out]
out_idx = s.index[mask_out]
stats = {"median": med, "MAD_raw": mad_raw, "threshold": threshold}
return cleaned, out_idx, stats
# ---------------- 사용 예시 ----------------
if __name__ == "__main__":
# 예시: 로그 수익률(단일 열)
log_return = pd.Series([0.01, 0.02, -0.01, 0.015, 0.018, 0.013, 0.50, np.nan])
# 1) IQR
cleaned_iqr, out_iqr, b = iqr_clean_series(log_return, k=1.5)
print("IQR 이상치 인덱스:", list(out_iqr))
print("IQR 경계:", b)
print("IQR 제거 후 길이:", len(cleaned_iqr))
# 2) MAD
cleaned_mad, out_mad, st = mad_clean_series(log_return, threshold=2.5)
print("MAD 이상치 인덱스:", list(out_mad))
print("MAD 통계:", st)
print("MAD 제거 후 길이:", len(cleaned_mad))
R
# --- IQR 기반 --------------------------------------------------------
iqr_clean_vector <- function(x, k = 1.5) {
x_num <- as.numeric(x)
q1 <- as.numeric(quantile(x_num, 0.25, na.rm = TRUE, type = 7))
q3 <- as.numeric(quantile(x_num, 0.75, na.rm = TRUE, type = 7))
iqr <- q3 - q1
lower <- q1 - k * iqr
upper <- q3 + k * iqr
mask_out <- (x_num < lower) | (x_num > upper)
mask_out[is.na(x_num)] <- FALSE # NA는 이상치로 보지 않음
cleaned <- x[!mask_out]
out_idx <- which(mask_out)
bounds <- list(Q1 = q1, Q3 = q3, IQR = iqr, lower = lower, upper = upper)
list(cleaned = cleaned, outlier_idx = out_idx, bounds = bounds)
}
# --- MAD(Modified Z-score) 기반 -------------------------------------
mad_clean_vector <- function(x, threshold = 2.5) {
x_num <- as.numeric(x)
med <- median(x_num, na.rm = TRUE)
mad_raw <- mad(x_num, center = med, constant = 1, na.rm = TRUE) # 보정 전 MAD
if (is.na(mad_raw) || mad_raw == 0) {
z <- rep(0, length(x_num))
mask_out <- rep(FALSE, length(x_num))
} else {
z <- 0.6745 * (x_num - med) / mad_raw
mask_out <- abs(z) > threshold
mask_out[is.na(x_num)] <- FALSE
}
cleaned <- x[!mask_out]
out_idx <- which(mask_out)
stats <- list(median = med, MAD_raw = mad_raw, threshold = threshold)
list(cleaned = cleaned, outlier_idx = out_idx, stats = stats)
}
# ---------------- 사용 예시 ----------------
log_return <- c(0.01, 0.02, -0.01, 0.015, 0.018, 0.013, 0.50, NA_real_)
# 1) IQR
res_iqr <- iqr_clean_vector(log_return, k = 1.5)
print(res_iqr$outlier_idx) # 이상치 위치
print(res_iqr$bounds) # 경계값
print(length(res_iqr$cleaned))
# 2) MAD
res_mad <- mad_clean_vector(log_return, threshold = 2.5)
print(res_mad$outlier_idx) # 이상치 위치
print(res_mad$stats) # 통계치
print(length(res_mad$cleaned))