[통계] "이상치는 무조건 제거 해야 한다."는 잘못된 생각

"이상치를 발견했다 = 무조건 제거한다"는 잘못된 생각입니다!

핵심 메시지

이상치는 '쓰레기'가 아니라 '특별한 신호'일 수 있습니다

 무작정 제거하기 전에 "우리 분석 목적에 필요한지 불필요한지"를 먼저 고민하세요.


자주 하는 실수들

[실수 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!