"이상치를 발견했다 = 무조건 제거한다"는 잘못된 생각입니다!
핵심 메시지
이상치는 '쓰레기'가 아니라 '특별한 신호'일 수 있습니다
무작정 제거하기 전에 "우리 분석 목적에 필요한지 불필요한지"를 먼저 고민하세요.
자주 하는 실수들
[실수 1] "통계가 시키는 대로"
"IQR에서 이상치라고 했으니 제거"
"Z-score가 3 넘으니 제거"
→ 도구는 '발견'용, 제거 결정은 '사람'이!
[실수 2] "깨끗한 데이터 강박"
"이상치 다 제거하니 데이터가 깔끔해졌어요"
→ 중요한 정보도 같이 날렸을 수도...
이상치 탐지 방법들
1. 통계적 방법
- IQR (Interquartile Range) 방법
- 원리: Q1 - 1.5×IQR 미만, Q3 + 1.5×IQR 초과 값
- 장점: 중앙값 기반이라 이상치에 강건함, 분포 가정 불필요
- 적합한 경우:
- Z-Score 방법
- 원리: 평균으로부터 ±3 표준편차 벗어난 값
- 장점: 수치적으로 명확한 기준
- 적합한 경우:
2. 도메인 기반 방법
Business Rule 방법
# 상식적/업무적으로 불가능한 값
나이 < 0 or 나이 > 150 # 인간 수명 한계
체온 < 30 or 체온 > 45 # 생존 불가능 범위
할인율 > 100 # 100% 초과 할인 불가능
이상치 처리 방법 5가지
1. 제거
# 오류 제거
df = df[df['age'] >= 0]
# 분석 목적상 제외
df_normal = df[df['purchase'] < 1000000] # 일반 고객만 분석
언제 사용?
- 명백한 오류나 불가능한 값
- 분석 목적과 맞지 않는 데이터
- 극소수 극단값이 전체 분석을 왜곡시킬 때
실무 예시
1. 오류: 나이 999살 → 제거
2. 분석 범위 밖: 해외 고객 분석에서 국내 고객 → 제거
3. 극단 왜곡: 1000명 중 1명이 평균의 100배 → 제거 고려
2. 대체
# 중앙값 대체
df.loc[outlier_idx, 'col'] = df['col'].median()
# 경계값 대체 (Winsorizing)
p95 = df['col'].quantile(0.95)
df.loc[df['col'] > p95, 'col'] = p95
# 그룹별 대체
df.loc[outlier_idx, 'salary'] = df.groupby('job')['salary'].transform('median')
언제 사용?
- 샘플 수 유지가 중요할 때 (작은 데이터셋)
- 시계열 연속성이 필요할 때
- 극단값을 완화하되 데이터는 보존하고 싶을 때
실무 예시
1. 시계열: 일별 매출에서 특정일만 비정상적으로 높음 → 상한선으로 대체
2. 소규모 데이터: 100개 중 10개 이상치 → 제거보다는 대체
3. 센서 오작동: 순간적 스파이크 → 이전/이후 값의 평균으로 대체
3. 변환
# 로그 변환 (강한 변환)
df['income_log'] = np.log1p(df['income'])
# 제곱근 변환 (중간 변환)
df['sales_sqrt'] = np.sqrt(df['sales'])
# Box-Cox 변환 (최적 변환)
from scipy import stats
df['transformed'], lambda_param = stats.boxcox(df['positive_values'])
언제 사용?
- 전체 분포를 정규분포에 가깝게 만들 때
- 변수 간 스케일 차이를 줄일 때
실무 예시
1. 소득 분석: 10만원 ~ 10억원 → 로그 변환으로 스케일 조정
2. 부동산 가격: 우측 긴 꼬리 분포 → 로그 변환
3. 인구 데이터: 도시별 인구 편차 큼 → 제곱근 변환
주의사항
0이나 음수 처리 필요 (log는 양수만 가능)
4. 분류 : 이상치를 별도의 그룹으로 분류
# 단순 분류
df['segment'] = pd.cut(df['amount'],
bins=[0, 100000, 1000000, float('inf')],
labels=['small', 'medium', 'large'])
# 비즈니스 룰 기반 구분
df['customer_type'] = 'normal'
df.loc[df['purchase'] > df['purchase'].quantile(0.95), 'customer_type'] = 'VIP'
df.loc[df['frequency'] > 30, 'customer_type'] = 'frequent'
# 각 세그먼트별 분석
for segment in df['segment'].unique():
segment_data = df[df['segment'] == segment]
# 세그먼트별 별도 분석
언제 사용?
- 이상치가 의미있는 별도 그룹일 때
- 다른 특성을 가진 여러 그룹이 혼재할 때
- 비즈니스적으로 구분이 필요할 때
- 제거는 아깝고 포함은 부담스러울 때
실무 예시
1. 고객 분석: VIP vs 일반 고객 별도 분석
2. 매장 분석: 플래그십 스토어 vs 일반 매장
3. 제품 분석: 프리미엄 라인 vs 일반 제품
5. 유지 (Keep as is) : 이상치를 유지하면서 전체 데이터를 분석하고자 하는 경우
# 그대로 유지하되 robust한 방법 사용
# Robust 통계량
median = df['col'].median() # 평균 대신 중앙값 활용
iqr = df['col'].quantile(0.75) - df['col'].quantile(0.25) # 표준편차 대신 사분위 범위 활용
# Robust 상관계수
spearman_corr = df.corr(method='spearman') # Pearson 대신 스피어만 상관계수 활용
언제 사용?
- 이상치 자체가 분석 대상일 때 (사기 탐지, 이상 감지)
- 전체 데이터의 실제 분포가 중요할 때
- 충분한 데이터가 있고 robust한 방법 사용 가능할 때
실무 예시
1. 리스크 분석: 극단적 손실 사례도 중요
2. 품질 관리: 불량품도 분석 대상
3. 전체 시장 현황: 모든 거래 포함 필요
최종 정리
"이상치를 제거하는 이유는 '이상치라서'가 아니라 '우리 분석 목적에 맞지 않아서'여야 합니다"
무작정 제거 NO! 논리적 판단 YES!
'통계 (Statistics)' 카테고리의 다른 글
| [통계/세션] 5회차 - 회귀(Regression) (0) | 2025.10.13 |
|---|---|
| [통계/Q&A] 가설검정 관련 질문 답변 (0) | 2025.10.12 |
| [통계] 마인드맵으로 통계 맥락 알아보기 👀 (1) | 2025.10.12 |