Search

이상치 확인, 제거하기

개요

이상치 탐지는 오류 식별과 함께 중요할 수 있는 데이터 포인트 발견 사이의 경계에 있음. 통계 기법과 알고리즘 접근법을 통해, z-score, IQR과 같은 단변량 방법부터 dbscan 과 lof과 같은 다변량 기법까지 다양한 이상치 탐지 방법의 이론과 응용을 학습

이상치의 유형

단변량 이상치
단 하나의 특성만 보고 이상치를 판단하는 경우
다변량 이상치
여러 특성값이 있는 상태에서 이상치를 판단하는 경우

단변량 이상치 탐지

z-score
데이터에서 표준 편차를 이용해 이상치를 감지하는 방법. 데이터를 표준화한 뒤, 각 데이터 포인트가 평균으로부터 얼만큼 떨어져있는가를 계산해 이상치를 식별.
IQR(사분위수범위)
데이터의 ‘중앙값’을 기준으로 ‘정상 범위’를 벗어난 데이터 포인트를 시각적으로 찾아냄. Q1(제1사분위수)와 Q3(제3사분위수)를 구한 뒤, 이를 기반으로 IQR을 계산해 이상치를 탐지

3.1. Z-Score의 해석

데이터가 정규 분포를 따른다고 가정할 경우 아래와 같이 해석할 수 있습니다.
Z-Score = 0: 데이터 포인트가 평균값에 정확히 일치
Z-Score > 0: 데이터 포인트가 평균값보다 높음
Z-Score < 0: 데이터 포인트가 평균값보다 낮음
→ 일반적으로, Z-Score가 매우 높거나 낮은 값(예를 들어, ±2 또는 ±3 이상)은 데이터 세트에서 이상치일 가능성이 높다고 간주된다. 이런 값들은 나머지 데이터와 상당히 다르기 때문에 특별한 주의가 필요하다.
Z-score 값이 -3 이하 또는 3 이상인 지역은 이상치로 간주되며, 붉은색으로 표시되어 있다.
-2와 2 사이의 값들은 이상하지 않은(normal) 범위로 간주되며, 노란색으로 표시되어 있습니다.
-2와 -3 사이, 2와 3 사이의 값들은 다소 이상하다고 여겨지는(moderately unusual) 범위로, 주황색으로 표시되어 있습니다.

Z-score 로 이상치 처리하기

1.
수동으로 계산하고, 제거하기
데이터 세트의 평균과 표준편차를 계산해 Z-score를 수동으로 계산한 뒤 임곗값을 넘는 데이터를 이상치로 처리하고 제거하기
2.
Scipy를 이용해 계산하고 제거하기
Scipy 라이브러리의 stats 모듈은 z-score계산을 자동화하는 함수를 제공.

데이콘 중고차 데이터셋으로 실습하기

1.
income_total 피처 분포 확인하기
plt.figure(figsize=(10,5)) sns.distplot(X_train['income_total']) plt.show()
Python
복사
2.
income_total 이상치 개수 확인하기
Z-score 계산하는 함수 직접 만들기
def out_zscore(data, threshold=3): mean = np.mean(data) std = np.std(data) zscores = [(x - mean) / std for x in data] outliers = [x for x in data if np.abs((x - mean) / std) > threshold] return zscores, len(outliers) _, num_outliers = out_zscore(X_train.income_total) print('Total number of outliers are", num_outliers)
Python
복사
결과 : Total number of outliers are 151 평균으로부터 +=3 표준편차 이상 떨어진 이상치가 151개 존재
Z-score 분포 그려보기 (distplot) : KDE분포함수
zscores, _ = out_zscore(X_train.income_total) plt.figure(figsize=(10,5)) sns.distplot(zscores) plt.axvspan(xmin=3, xmax=max(zscores), alpha=0.2, color='red') plt.show()
Python
복사
plt.axvspan 함수를 이용해서 데이터값이 3이상인 공간을 투명도 0.2인 빨간색으로 칠해줬다. : 수직 영역을 강조할 때 사용됨.
임곗값 3을 넘는 이상치들 수동으로 제거해주기
Z_train = X_train.copy() # Train 데이터의 평균, 표준편차 계산 mean_train = Z_train['income_total'].mean() std_train = Z_train['income_total'].std() # 임계값 설정 threshold = 3 # Train 데이터로 Z-점수 계산 및 이상치 제거 Z_train['z_score_income'] = (Z_train['income_total'] - mean_train) / std_train train_no_outliers = Z_train[Z_train['z_score_income'].abs() <= threshold] train_no_outliers = train_no_outliers.drop('z_score_income', axis=1) train_no_outliers.shape
Python
복사
결과 : (11050, 19)
이상치를 제거하고 11050개의 데이터가 남은 것을 확인했다.
Scipy 라이브러리를 이용한 z-score 계산과 이상치 제거
stats 모듈을 사용한다.
from scipy import stats Z_scipy_train = X_train.copy() # z-score 계산 Z_scipy_train['z_score_income'] = stats.zscore(Z_scipy_train['income_total'] # 임곗값 설정 threshold = 3 # 임곗값을 기준으로 이상치 제거 train_no_outliers = Z_scipy_train[Z_scipy_train['z_score_income'].abs() <= threshold] # z_score_income 컬럼 제거 train_no_outliers = train_no_outliers.drop('z_score_income', axis=1) train_no_outliers.shape
Python
복사
stats 모듈의 zscore 함수를 사용한다.
결과 : (11050, 19)

Z-score 의 장단점

장점
간단하고 직관적
표준화된 형태로 표현되 서로 다른 데이터셋이나 변수 간 비교가 용이
데이터가 정규 분포를 따를 경우, 매우 효과적
단점
데이터의 분포가 비대칭이거나 긴 꼬리를 가진 형태일 경우, 오류가 발생할 수 있음
이상치가 있을 경우, 평균과 표준편차에 영향을 미쳐 z-score계산에도 영향을 미침
데이터에 이상치가 많을 경우, 효과적이지 못할 수 있음
임곗값 설정이 주관적이며 이에 따라 결과가 달라질 수 있음

3.2.IQR로 이상치 처리하기

IQR은 데이터 세트의 중간 50% 범위를 측정하는 통계적 방법임.
IQR은 제 1사분위수(25%)와 제 3사분위수(75%) 사이의 차이로 정의됨
중앙값에 대한 분산을 나타내며, 이상치 탐지에 매우 유용
IQR = Q3 - Q1
이상치 하한 경계는 Q1 - 1.5 * IQR, 상한 경계는 Q3 + 1.5 * IQR
위의 경계를 벗어나는 데이터를 일반적으로 이상치로 간주한다.
Quantile 함수로 IQR 이상치 제거하기
pandas 라이브러리의 quantile 함수를 이용해 Q1, Q3 를 구하고 IQR를 계산할 수 있다.
Percentile 함수로 IQR 이상치 제거하기
Numpy 라이브러리의 percentile 함수를 이용해 백분위수를 구하고, IQR을 계산할 수 있다.
상자 그림 : Boxplot
plt.boxplot(X_train['income_total'], vert=False) # vert=False 는 상자그림을 가로로 plt.title('Boxplot for Feature income_total') plt.show()
Python
복사
IQR로 특성값의 이상치 개수를 계산하는 함수 만들기
1.
quantile 함수 (pandas)
def out_iqr(data): Q1 = data.quantile(0.25) Q3 = data.quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - 1.5*IQR upper_bound = Q3 + 1.5*IQR return lower_bound, upper_bound lower_bound, upper_bound = out_iqr(X_train['income_total']) lower_outliers = X_train[X_train['income_total'] < lower_bound] upper_outliers = X_train[X_train['income_total'] > upper_bound] # 이상치 개수 세기 num_lower_outliers = len(lower_outliers) num_upper_outliers = len(upper_outliers) print("Number of lower outliers in 'income_total' column:", num_lower_outliers) print("Number of upper outliers in 'income_total' column:", num_upper_outliers)
Python
복사
Number of lower outliers in 'income_total' column: 0
Number of upper outliers in 'income_total' column: 467 하위 이상치는 발견되지 않았고, 상위 이상치가 467개라는 것을 확인
이상치 제거하기
lower_bound, upper_bound = out_iqr(X_train['income_total']) train_no_outliers_iqr = X_train[(X_train['income_total'] >= lower_bound) & (X_train['income_total'] <= upper_bound)] print("Shape of train data after removing outliers using IQR:", train_no_outliers_iqr.shape)
Python
복사
Shape of train data after removing outliers using IQR: (10734, 19)
2.
percentile 함수 (numpy)
import numpy as np Q1 = np.percentile(X_train['income_total'],25) Q3 = np.percentile(X_train['income_total'],75) IQR = Q3 - Q1 lower_bound = Q1 - 1.5*IQR upper_bound = Q3 + 1.5*IQR train_no_outliers_iqr_np = X_train[(X_train['income_total'] >= lower_bound)&(X_train['income_total'] <= upper_bound)] print("Shape of train data after removing outliers using NumPy's percentile:", train_no_outliers_iqr_np.shape)
Python
복사
이상치 제거한 뒤, boxplot
plt.boxplot(train_no_outliers_iqr['income_total'], vert=False) plt.title('Boxplot for Feature income_total') plt.show()
Python
복사

IQR 의 장단점

장점
극단치에 덜 예민하다 → 데이터의 분포가 비대칭일 때 유용
직관적
비모수적 접근 가능 → 즉, 정규 분포를 따르지 않아도 사용 가능하기 때문에 실제 상황에 더 유용
단점
이상치를 정의하는 임곗값 (1.5*IQR)은 어느 정도 임의적임. 명확한 가이드라인이 없음

이상치를 꼭 제거해야 될까?

결과적으로 이상치는 꼭 제거해야 될 필요는 없다.
이상치를 다루는 다른 방법들이 고려되고 있다.
데이터 변환
로그 변환, 제곱근, 역수 취하기 등으로 분포를 바꿀 수 있음
이상치 점수화
이상치를 제거하지 않고, 이상치를 식별한 뒤 점수화한다. 모델링에 활용하거나 이상치 정도를 파악할 수 있음
이상치 치환
이상치를 다른 값으로 대체한다. 중앙값이나 평균값, 근삿값 등
모델 기반 접근
일부 머신 러닝 모델은 이상치를 알아서 처리한다. 랜덤 포레스트와 같은 트리 기반 모델은 이상치에 민감하지 않은 경우도 있다.
이상치를 고려한 모델링
오히려 이상치가 중요한 정보를 제공하는 경우에는 결과에 미치는 영향을 분석할 수 있다.

다변량 이상치 탐지

DBSCAN
데이터들을 그룹화해서 이상치를 식별하는데 사용되는 클러스터링 알고리즘.
데이터 포인트의 밀도를 기반으로 주변 데이터 포인트와의 관계를 고려하여 이상치를 찾아냄.
다양한 데이터 구조에서 유용함.
LOF
데이터 포인트의 ‘지역적 밀도’를 측정하고, 이를 주변 포인트의 밀도와 비교해 이상치를 탐지.
데이터의 지역적 밀도가 다를 때 효과적.

DBSCAN 이용해서 이상치 탐지하기

클러스터를 형성한 뒤, 여기에 속하지 않는 데이터들을 이상치로 간주하는 알고리즘
K-means 알고리즘의 대안으로 사용되며, 미리 클러스터의 개수를 정하지 않고 데이터 자체의 밀도에 기반해 클러스터링을 수행한다.
몇 가지 key point는
핵심 포인트
주변 지역(eps 거리 내)에 일정 수(min_samples) 이상의 이웃 포인트가 있는 데이터 포인트
이 포인트들은 클러스터링 과정에서 클러스터의 '핵심'을 형성
경계 포인트
핵심 포인트의 이웃이지만, 그 자체로는 핵심 포인트의 조건(즉, min_samples 이상의 이웃)을 충족하지 않는 포인트
클러스터의 ‘경계’를 형성
노이즈 포인트
핵심 포인트나 경계 포인트가 아닌 모든 포인트
데이터셋에서 ‘이상치’를 나타냄
다변량 데이터들의 산점도 분석
ex) ‘공복 혈당’ 과 ‘중성 지방’ 두 변수의 산점도를 생성해서 관계를 시각화한다.
import matplotlib.pyplot as plt plt.figure(figsize=(10,6)) plt.scatter(X_train['공복 혈당'], X_train['중성 지방']) plt.title('공복 혈당과 중성 지방의 scattor plot') plt.xlabel('공복 혈당') plt.ylabel('중성 지방') plt.grid(True) plt.show()
Python
복사
DBSCAN으로 이상치 탐지하기
< 사전 작업 >
두 변수 표준화하기
DBSCAN 알고리즘 적용하기
from sklearn.cluster import DBSCAN from sklearn.preprocessing import StandardScaler DBSCAN_train = X_train.copy() numeric_columns = ['공복 혈당','중성 지방'] data_numeric = DBSCAN_train[numeric_columns] # 데이터 표준화 scaler = StandardScaler() data_scaled = scaler.fit_transform(data_numeric) db = DBSCAN(eps=0.5, min_samples=5).fit(data_scaled) #eps : 입실론반경 , min_samples : 입실론 반경 최소 샘플 수 labels = db.lables_ pd.Series(labels).value_counts()
Python
복사
0 4839 -1 42 1 14 2 5 dtype: int64
Plain Text
복사
DBSCAN 알고리즘은 데이터 포인트에 클러스터 라벨을 할당한다. -1 : 이상치
클러스터링 결과 시각화
plt.figure(figsize=(10,6)) unique_labels = set(labels) color = ['blue','red'] for color, label in zip(color, unique_labels): if label == -1: # 이상치는 빨간색으로 표시 outlier_color = 'red' else: # 클러스터에 속한 데이터는 파란색으로 표시 cluster_color = 'blue' sample_mask = labels == label plt.plot(data_numeric.iloc[sample_mask,0], data_numeric.iloc[sample_mask,1], 'o', color=color) # x축과 y축에 라벨 추가 plt.xlabel('공복 혈당', fontsize=10) plt.ylabel('중성 지방', fontsize=10) plt.title('공복 혈당과 중성 지방에 대한 DBSCAN 클러스터링', fontsize=10) plt.grid(True) plt.show()
Python
복사
이상치 제거 결과
클러스터 레이블 추가
이상치가 아닌 데이터 선택
clusters_sample = db.fit_predict(data_scaled) DBSCAN_train['clusters'] = clusters_sample sample_no_outliers = DBSCAN_train[DBSCAN_train['clusters']!= -1] display(sample_no_outliers.head(3)) display(f"이상치가 아닌 데이터 포인트들의 수: {len(sample_no_outliers)}")
Python
복사
'이상치가 아닌 데이터 포인트들의 수: 4858'
plt.figure(figsize=(10,6)) plt.scatter(DBSCAN_train['공복 혈당'], DBSCAN_train['중성 지방'], c=DBSCAN_train['clusters'], cmap='viridis', label='Clusters') # cluster별로 데이터 찍기 plt.scatter(DBSCAN_train[DBSCAN_train['clusters'] == -1]['공복 혈당'], DBSCAN_train[DBSCAN_train['clusters'] == -1]['중성 지방'], color = 'red', label = 'Outliers') # 이상치 데이터 다시 찍기 plt.title('공복 혈당과 중성 지방의 scatter plot') plt.xlabel('공복 혈당') plt.ylabel('중성 지방') plt.legend() plt.grid(True) plt.show()
Python
복사

DBSCAN 장단점

장점
모델 가정 불필요
클러스터 개수 설정 불필요
이상치에 영향 안받음
파라미터 설정으로 유연하게 조정
eps , min_samples
단점
파라미터 설정에 따라 결과가 달라져 여러 시도 필요
변수 스케일에 영향, 사전 데이터 처리 필요
차원의 저주로 고차원 데이터에서 성능이 저하될 수 있음
밀도가 크게 변할 경우, 한가지 eps로 모든 클러스터를 식별하기 힘듦
매우 다양한 크기의 클러스터가 존재할 경우, 모든 클러스터를 동일하게 잘 식별하지 못할 수 있음

LOF 를 활용해서 이상치 탐지하기

LOF(local outlier factor)는 주변 이웃과 비교해 데이터가 얼마나 이상한지를 평가하는 알고리즘
각 데이터들의 지역 밀도를 계산하고, 주변 이웃 데이터들의 밀도와 비교.
LOF 점수는 데이터 포인트가 주변 이웃과 얼마나 다른지를 나타냄. 값이 클수록 이상치일 가능성이 큼
 Key Point
이웃 개수 설정
고려할 이웃 데이터의 개수를 설정(n_neighbors) → 얼마나 지역적으로 데이터를 보는지 결정
지역 밀도 계산
가장 가까운 n_neighbors 이웃들까지의 거리의 평균을 계산.
LOF 점수 계산
점수가 1에 가까울 수록 이웃들과 비슷한 밀도를 가지고 있는 것.
1보다 높으면 높을수록 이상치 가능성이 높아짐
이상치 결정
LOF 값이 특정 임곗값 이상이면 이상치로 분류. 경험적으로 결정하거나 비율로 설정
LOF 알고리즘으로 이상치 판별하기
LocalOutlierFactor 알고리즘은 배열을 입력으로 받는다 → numpy 사용
from sklearn.neighbors import LocalOutlierFactor # 데이터 준비 LOF_train = X_train.copy() numeric_columns = ['공복 혈당', '중성 지방'] data_numeric = LOF_train[numeric_columns].values # 넘파이 배열로 변환 # Local Outlier Factor 모델 생성 clf = LocalOutlierFactor(n_neighbors=50, contamination='auto') #이웃수:50, contamination:이상치의 비율 설정 -> 'auto':자동으로 비율 추정 # 이상치 탐지 labels = clf.fit_predict(data_numeric) # 학습과 예측 레이블의 결과 pd.Series(labels).value_counts()
Python
복사
1 4781 -1 119 dtype: int64
Plain Text
복사
정상 데이터 포인트는 1, 이상치 데이터 포인트는 -1로 레이블됨. 119개의 데이터 포인트가 이상치로 분류되었고, 주변 이웃에 비해 상대적으로 낮은 밀도를 갖고 있는 것으로 판단됨.
이상치 데이터 필터링하기
LOF_train['outliers_lof'] = labels sample_no_outliers = LOF_train[LOF_train['outliers_lof'] != -1] display(sample_no_outliers.head(3)) display(f"이상치가 아닌 데이터 포인트들의 수: {len(sample_no_outliers)}")
Python
복사
'이상치가 아닌 데이터 포인트들의 수: 4781'
LOF 를 이용한 이상치 시각화
plt.figure(figsize=(10, 6)) plt.scatter(LOF_train['공복 혈당'], LOF_train['중성 지방'], c=LOF_train['outliers_lof'], cmap='viridis', label='Clusters') plt.scatter(LOF_train[LOF_train['outliers_lof'] == -1]['공복 혈당'], LOF_train[LOF_train['outliers_lof'] == -1]['중성 지방'], color='red', label='Outliers') plt.title('공복 혈당과 중성 지방의 scatter plot') plt.xlabel('공복 혈당') plt.ylabel('중성 지방') plt.legend() plt.grid(True) plt.show()
Python
복사

LOF 장단점

장점
지역적 관점에서 이상치를 식별하기 때문에 밀도에 영향을 받지 않음
다양한 데이터셋에 적용 가능. 밀도가 불균형한 데이터셋에서도 유용함
이상치의 순위화 : 이상치로 판단되는 정도를 수치로 제공.
파라미터 조정의 유연성 : 이웃의 수를 조정해서 모델의 민감도를 조절할 수 있음
단점
파라미터 조정의 어려움 : 파라미터 조정에 따라 정확도가 달라지기 때문에 경험치가 필요함
고차원 데이터의 한계 : 차원의 저주에 영향을 받을 수 있음
계산 복잡성 : 큰 데이터셋의 경우, 데이터 포인트의 이웃을 찾는 과정이 복잡할 수 있음
스케일링 필수
특정 변수의 데이터 분포가 한쪽으로 심하게 치우쳐져 있는 문제가 생길 경우 데이터 분석을 하기 앞서 정비해줄 필요가 있다.
방식에는 여러가지가 있는데, 다음과 같다.

threshold 를 정해서 데이터 잘라내기

1) 양쪽 사이드에서 몇 %를 제거해내는 방법
min_threshold, max_threshold = df['var'].quantile([0.01,0.99]) # 양쪽 사이드 1% 에 해당하는 값 # 위의 범위 사이에 존재하는 데이터들만 뽑아내기 df[(bnb['var'] > min_threshold)&(bnb['var'] < max_threshold)]
SQL
복사