이상치란?

변수 범위에서 많이 벗어난 아주 작은 값이나 아주 큰 값으로, 일반화된 모델을 생성하는데 악영향을 끼치는 값으로 이상치를 포함하는 레코드를 제거하는 방법으로 이상치를 제거한다.

이상치는 결측치와 다르게 값을 추정을 하는 것이 아니라 제거해야한다.(절대 추정의 대상이 아님에 주의한다.)

 

이상치를 제거해야하는 대표적인 모델들은 클래스의 평균을 쓰는 모델들이다.(트리계열 모델, 거리를 사용하는 모델:KNN, 회귀모델(회귀모델도 학습될 때 미분 하는 과정에서 평균을 사용)) →이런 모델들은 이상치를 제거해야만 일반화된 모델을 만들수 있다.

위의 그림에서 총 7개의 레코드가 있다. 빨간색 레코드는 다른 레코드에 비해서 많이 벗어나있다.

회귀 모델을 만들었는데 빨간색 레코드의 영향을 받아서 일반적인 다른 레코드를 설명하지 못하고 위쪽으로 쏠렸다.

빨간색 레코드인 이상치를 제거한 다음에 모델을 만들면 일반화된 모델이 만들어질 것이다.

 

이상치 판단 방법 1. IQR 규칙 활용

변수별로 IQR 규칙을 만족하지 않는 샘플들을 판단하여 삭제하는 방법이다.

직관적이고 사용이 간편하다는 장점이 있지만, 단일 변수로 이상치를 판단하기 어려운 경우가 있다는 문제가 있다.

즉, 단일 변수로 보면 이상치이지만 여러개의 변수로 같이 보면 이상치가 아니거나, 단일 변수로 보면 이상치가 아니지만 여러개의 변수로 같이 보면 이상치인 경우가 있다는 말이다. 이럴 경우 IQR을 사용하게 되면 완벽히 무시되는 단점이 있다.

결론: IQR 기준만 가지고 이상치라고 단언할 수는 없다.

(TIP) IQR rule이라는 것은 boxplot을 그릴 때 점으로 표시된 것들이 IQR rule에 의한 이상치이다.

 

  • 관련 문법: numpy.quantile

array의 q번째 quantile(분위수)을 구하는 함수이다.

-주요 입력

▷a: input array(list, ndarray등)

▷q: quantile(0과 1사이)

 

 

## 코드 실습 ##

import os
os.chdir(r"C:\Users\82102\Desktop\데이터전처리\머신러닝 모델의 성능 향상을 위한 전처리\5. 머신러닝 모델의 성능 향상을 위한 전처리\데이터")

import pandas as pd
df = pd.read_csv("glass.csv")
df.head()

# 특징과 라벨 분리
X = df.drop('Glass_type', axis=1)
Y = df['Glass_type']
# 학습 데이터와 평가 데이터 분리
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(X, Y)
x_train.shape

x_train1 = x_train.copy() # 이상치 비율을 조정할수도 있으니 복사본 생성
import numpy as np
def IQR_rule(val_list): # 한 특징에 포함된 값(열 벡터)
    # IQR 계산
    Q1 = np.quantile(val_list, 0.25) # 제 1사분위수(25%)
    Q3 = np.quantile(val_list, 0.75) # 제 3사분위수(75%)
    IQR = Q3-Q1
    
    # IQR rule을 위배하지 않는 bool list 계산(True: 이상치 x, False: 이상치 0)
    not_outlier_condition = (Q3 + 1.5 * IQR > val_list) & (Q1 - 1.5 * IQR < val_list)
    return not_outlier_condition
# apply를 이용하여 모든 컬럼에 IQR rule 함수 적용
conditions = x_train.apply(IQR_rule)
conditions

# 하나라도 IQR 규칙을 위반하는 요소를 갖는 레코드를 제거하기 위한 규칙
total_condition = conditions.sum(axis=1) == len(x_train.columns)
x_train = x_train.loc[total_condition] # 이상치 제거
x_train

x_train.shape # 45개 삭제됨

이상치 비율이 45 / 160 = 0.28125로 약 30프로이다.

그렇다면 이상치가 30프로라는게 일반적인 수치라고 볼 수 있을까?

전체 데이터에서 이상치가 30프로라는 것은 사실 말이 안된다. 그래서 이 수치는 이상치 비율이라고 보기에는 매우 높다.

정답은 없지만 일반적으로 이상치의 비율은 1프로 미만이다.  

 

위에서 (Q3 + 1.5 * IQR > val_list) & (Q1 - 1.5 * IQR < val_list)로 IQR_rule을 계산했는데 1.5는 절대적인 수치가 아니므로 이를 조절해도 무방하다. 이 수치를 크게 둘수록 이상치의 비율이 떨어진다.
그래서 이상치의 비율이 1프로 미만이 되도록 조정하는 것이 더 바람직하다.

def IQR_rule(val_list): # 한 특징에 포함된 값(열 벡터)
    # IQR 계산
    Q1 = np.quantile(val_list, 0.25) # 제 1사분위수(25%)
    Q3 = np.quantile(val_list, 0.75) # 제 3사분위수(75%)
    IQR = Q3-Q1
    
    # IQR rule을 위배하지 않는 bool list 계산(True: 이상치 x, False: 이상치 0)
    not_outlier_condition = (Q3 + 6 * IQR > val_list) & (Q1 - 6 * IQR < val_list)
    return not_outlier_condition
# apply를 이용하여 모든 컬럼에 IQR rule 함수 적용
conditions = x_train1.apply(IQR_rule) # axis=0
conditions
# 하나라도 IQR 규칙을 위반하는 요소를 갖는 레코드를 제거하기 위한 규칙
total_condition = conditions.sum(axis=1) == len(x_train1.columns)
x_train1 = x_train1.loc[total_condition] # 이상치 제거
x_train1
x_train1.shape # 3개 삭제됨

이상치의 비율이 약 1프로이다.(3 / 160)

 


이상치 판단 방법 2. 밀도 기반 군집화 활용

밀도 기반 군집화 기법은 군집에 속하지 않은 샘플을 이상치라고 간주하므로, 밀도 기반 군집화 결과를 활용하여 이상치를 판단할 수 있다. 이 방법의 대표적인 알고리즘이 DBSCAN이다.

이 방법은 단일 변수로 이상치를 판단하는 IQR_rule과 다르게,

다른 특징과의 관계까지 반영할 수 있다는 장점이 있다. 하지만 DBSCAN 등의 밀도 기반 군집화 모델은 파라미터 튜닝이 쉽지 않다는 단점이 있다.(DBSCAN으로 모델을 만들 때 eps, min_samples와 같은 파라미터 튜닝이 어렵다.)

 

  • 관련 문법: sklearn.cluster.DBSCAN

DBSCAN 군집화를 수행하는 인스턴스를 생성하는 함수이다.

-주요 입력

▷eps(앱실론): 이웃이라 판단하는 반경

▷min_samples: 중심점이라 판단하기 위해, eps 내에 들어와야 하는 최소 샘플 수

▷metric: 사용하는 거리 척도

-주요 attribute

▷.labels_: 각 샘플이 속한 군집 정보(-1이면 이상치)

 

 

## 코드 실습 ##

import os
os.chdir(r"C:\Users\82102\Desktop\데이터전처리\머신러닝 모델의 성능 향상을 위한 전처리\5. 머신러닝 모델의 성능 향상을 위한 전처리\데이터")

import pandas as pd
import numpy as np
df = pd.read_csv("glass.csv")
df.head()

# 특징과 라벨 분리
X = df.drop('Glass_type', axis=1)
Y = df['Glass_type']
# 학습 데이터와 평가 데이터 분리
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(X, Y)
x_train.shape

from scipy.spatial.distance import cdist
from sklearn.cluster import DBSCAN

 

cdist를 불러온 이유는 DBSCAN의 파라미터를 조정할 때 참고하기 위해서 불러왔다. 앱실론이라는 것은 결국 거리일텐데 그 거리가 데이터의 스케일마다 차이가 크기 때문에 거리를 미리 판단하기 쉽지 않아서 참고하고자 cdist를 불러왔다.

즉, cdist를 이용하면 쉽게 데이터간에 거리를 구할 수 있다.

# x_train과 x_train 거리 행렬계산 => DBSCAN의 파라미터를 설정하기 위함
DM = cdist(x_train, x_train)
DM

np.quantile(DM, 0.1)
# 샘플 간 거리의 10% quantile이 0.6455정도임을 확인

여기서 10%는 작은값을 기준으로 상위 10%를 말하는 것이다. 즉, 작은값을 기준으로 상위 10%의 거리가 0.6455정도 라는 것이다. 0.6455라고 나온값은 단순히 참고하는 수치이고 이게 정확한 기준은 아니다.
np.mean(DM), np.min(DM)처럼 평균이나 최솟값을 쓰기에는 대각행렬은 같은 레코드끼리 거리기 때문에 전부 0이고 대각 행렬 기준 대칭으로 값이 같기 때문에 평균이나 최솟값을 쓰기 어렵다. 그래서 분포통계량인 quantile을 사용했다.

cluster_model = DBSCAN(eps=0.6455, min_samples=3).fit(x_train)
print(sum(cluster_model.labels_ == -1))
# 33개가 이상치로 판단

33개가 이상치로 판단됐다. 이 정도면 너무 많은 양의 이상치라고 판단되어서 파라미터 조정을 하기로 결정했다.

cluster_model = DBSCAN(eps=2, min_samples=3).fit(x_train) # eps=2로 파라미터 조정
print(sum(cluster_model.labels_ == -1))

앱실론을 크게 늘리면 당연히 반경내에 샘플이 많이 들어갈 가능성이 높다. 그래서 이상치라고 판단되는 데이터의 수도 줄어들 것이다.

결과  5 / 160으로 이상치 비율이 약 3프로로 일반적인 이상치 비율인 1프로 미만보다 많은 수치이긴 하지만 이 정도면 괜찮겠다라고 판단했다. 즉, 5개 정도면 괜찮은 양이라고 판단하여 삭제를 한다.

x_train = x_train[cluster_model.labels_ != -1]
x_train.shape

 


=>학습 데이터에서 이상치를 제거해서 일반화된 모델을 만든 후 평가 데이터로 모델 성능 평가

변수 분포 문제란?

일반화된 모델을 학습하는데 어려움이 있는 분포를 가지는 변수가 있어서, 일반화된 모델을 학습하지 못하는 문제이다.

변수 분포 문제를 해결하지 않아도 모델 학습 자체는 가능하지만 좋은 성능을 기대하기 어렵다.

변수 분포 문제의 대표적인 다섯가지를 알아볼 것이다.

1) 특징과 라벨 간 약한 관계 또는 비선형 관계일 때

2) 이상치를 포함하는 데이터가 있을 때

3) 특징 간 상관성이 높아서 발생하는 문제

4) 변수가 정규분포를 따르지 않고 일반분포(알수없는 분포)를 따르는 경우(변수 치우침 제거) 

5) 변수 간 스케일 차이가 커서 발생하는 문제(스케일링)

 

특징과 라벨 간 약한 관계 또는 비선형 관계일 때

특징과 라벨 간 관계가 없거나 매우 약하다면, 어떠한 전처리 및 모델링을 하더라도 예측력이 높은 모델을 만들 수 없다.

 

가장 이상적인 해결 방안은 라벨에 유의미한 특징의 데이터를 구하는 것이다.

그 다음 이상적인 해결 방안은 각 특징에 대해, 특징과 라벨 간 관계를 나타내는 그래프를 통해 적절한 특징 변환을 수행해야 한다.(특징과 라벨 간 비선형 관계가 존재한다면, 적절한 전처리를 통해 모델 성능을 크게 향상시킬 수 있음)

예를들어, 대다수의 머신러닝 모델은 선형식(wx+b)을 포함하는데(선형회귀모델, 로지스틱회귀모델,  svm, mlp) 선형회귀 모델에서 데이터랑 초록색 모델은 거의 일치하는 것을 볼수있으나 파란색 모델은 실제 데이터의 분포를 거의 반영하지 못한다. 모델1은 X를 그대로 사용하고 모델2는 X^2을 사용한 것 밖에 차이가 없지만 단순한 변환임에도 불구하고 데이터의 X와 Y간 비선형 관계가 있는 경우(특징과 라벨 간 비선형 관계가 있는 경우)에는 특징 변환을 해줬을 때 엄청난 퍼포먼스를 얻을 수 있다.

→MAE나 RMSE는 구하지 않아도 당연히 초록색 모델의 성능이 훨씬 우수할 것으로 예상한다.

(주요 모델의 구조 포스팅 참고)

하지만 특징 개수가 많고, 다른 특징에 의한 영향도 존재하는 등 그래프를 통해 적절한 변환 방법을 선택하는 것은 쉽지 않다.→현실적으로 어려움

 

결론: 다양한 변환 방법(x제곱도 만들어보고, 로그x도 만들어보고, exp(x)도 만들어보고, 제곱근도 만들어보고, 아니면 두 컬럼을 더해보고, 빼보고 하는 등의 특징들을 만들어보고)을 시도하여 특징을 생성한 뒤 모델의 성능이 좋아지는 것을 고르면되고, 그 가운데 특징이 너무 많이 생기니 특징 선택까지 포함시키면 문제를 해결할수 있다.

즉, 다양한 변환 방법을 사용하여 특징을 생성한 뒤 특징 선택을 수행해야 한다.(특징 선택은 추후 포스팅에서 공부할 예정이다.)

 

 

## 코드 실습 ##

이번 실습은 기존 특징들을 가지고 다양한 변환 방법을 사용하여 새로운 특징들을 임의로 생성해서 모델링을 해보고, 기존 특징들만 가지고 모델링을 한 결과와 비교를 위한 실습이다.

import os
os.chdir(r"C:\Users\82102\Desktop\데이터전처리\머신러닝 모델의 성능 향상을 위한 전처리\5. 머신러닝 모델의 성능 향상을 위한 전처리\데이터")
import pandas as pd
import numpy as np
df = pd.read_csv("Combined_Cycle_Power_Plant.csv")
df.head()

# 특징과 라벨 분리
X = df.drop('EP', axis=1)
Y = df['EP']
# 모든 피쳐가 연속형
for i in X.columns:
    print(i, len(df[i].unique()))

# 신규 데이터 생성
# 특징이 추가된 데이터를 부착할 데이터
X_added = X.copy()

# 로그, 제곱 관련 변수 추가
# 만약, 범주형 변수와 연속형 변수가 혼합인 경우 나였으면 연속형 변수만 빼서 변수 추가 후 합쳤을듯...
for col in X.columns:
    X_added[col + '_squared'] = X[col]**2
    X_added[col + '_log'] = np.log(X[col])    

X_added.head()

(범주형 변수의 경우 원핫인코딩 후 로그변환을 하게되면 값이 달라져서 원본 데이터 손상이 되고, 제곱을 해도 똑같은 값이므로 연속형 변수만 특징변환?)

from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LinearRegression as LR

# 5겹 교차검증 기반의 평가 수행
X_score = -cross_val_score(LR(), X, Y, cv=5, scoring='neg_mean_absolute_error').mean()
X_added_score = -cross_val_score(LR(), X_added, Y, cv=5, scoring='neg_mean_absolute_error').mean()

# 특징을 추가했을 때 성능이 좋아졌다.
print("특징 추가 전: {}, 특징 추가 후: {}".format(X_score, X_added_score))

위 처럼 다양한 변환 방법을 사용하여 특징을 생성한 뒤 특징 선택을 수행

데이터에 범주형 변수가 포함되어 있을 때 문제점

● 데이터에 범주형 변수가 포함되어 있을 경우 대다수의 지도학습 모델이 학습되지 않거나 비정상적으로 학습이 된다.

▷str 타입의 범주형 변수가 포함되면 대다수의 지도학습 모델 자체가 학습이 되지 않는다. str 타입의 범주형 변수가 포함되더라도 그나마 정상적으로 학습되는 모델은 트리 계열 모델밖에 없다.

▷int 혹은 float 타입의 범주형 변수 또한 모델 학습이 가능하지만 비정상적으로 학습이 되기 때문에 적절한 범주형 변수 처리를 해야 한다. 

 

● 모델 학습을 위해 범주형 변수는 반드시 숫자로 변환되어야 하지만, 임의로 설정하는 것은 매우 부적절하다.

예시) 종교 변수: 기독교=1, 불교=2, 천주교=3

→불교는 기독교의 2배라는 등의 대수 관계가 실제로 존재하지 않지만, 이런식으로 변환하면 비정상적인 관계가 생기기 때문에 모델에 악영향을 끼칠수 밖에 없다.

 

● 코드화된 범주형 변수도 적절한 숫자로 변환해줘야 한다.

 

결론: 문자로 된 범주형 변수, 코드화된 범주형 변수를 적절한 숫자로 변환해줘야 한다.

 


범주형 변수 판별 방법

범주형 변수는 상태 공간의 크기가 유한한 변수이다. 

주의할 점은 int 혹은 float 타입으로 정의된 변수는 반드시 연속형 변수가 아닐 수 있다.

즉, 데이터의 type을 보고 "type이 float이니까 연속형, type이 str이니까 범주형" 이런식으로 판단하면 안된다.

그래서 반드시 도메인이나 변수의 상태 공간을 바탕으로 판단해야 한다.

 

예를 들어, 월(month), 시간(hour) 숫자로 되어있지만 범주형 변수이다.

 


범주형 변수 변환 방법 1. 더미화

가장 일반적인 범주형 변수를 변환하는 방법으로, 범주형 변수가 특정 값을 취하는지 여부를 나타내는 더미변수를 생성하는 방법이다.

위에 그림을 부과적으로 설명하자면,

불교 변수는 나머지 변수로 완벽히 추론 가능하므로 기독교, 천주교, 불교 이 세 변수 간에는 상관성이 완벽하게 존재한다. 특징 간 상관성이 완벽하게 존재하는 경우 정상적인 모델 학습이 어려울수 있기 때문에 상관성 제거를 위해 불교 변수를 제거한다.(기독교, 천주교, 불교 제거 / 기독교, 불교, 천주교 제거 / 천주교, 불교, 기독교 제거 중 하나로) 

예외로, 트리계열의 모델을 사용하는 경우에는 설명력을 위해서 변수를 제거 안하는 경우가 있다. 이런 경우 설명력을 위해서 그런거지 예측력을 위해서는 좋은 접근이 아니다.

 

단점: 한 범주형 변수가 상태 공간이 클 때 더미화를 할 경우 추가되는 변수가 엄청나게 많아질수 있다. 예를 들어, 어떤 범주형 변수의 상태 공간의 크기가 100일 때 100개의 변수가 추가된다. 이렇게 변수가 많아지면 차원의 저주 문제로 이어질수 있다. 그리고 데이터가 sparse(희소)해 질수도 있다.

결론적으로 범주형 변수의 상태 공간이 클때는 더미화를 하는게 적합하지 않다.

 

 

범주형 변수 변환 방법 2. 연속형 변수로 치환

범주형 변수의 상태 공간 크기가 클때, 더미화는 과하게 많은 변수를 추가해서 차원의 저주 문제로 이어질 수 있다.

 

라벨 정보(분류, 예측 둘다 가능)를 활용하여 범주 변수를 연속형 변수로 치환하면 기존 변수가 가지는 정보가 일부 손실될 수 있고 활용이 어렵다는 단점이 있으나, 차원의 크기가 변하지 않으며 더 효율적인 변수로 변활할 수 있다는 장점이 있다.

 이 방법을 쓸 때 장점은 더미화를 하는 것과 다르게 차원이 늘어나지 않는다. 그리고 범주형 변수를 라벨과 관련된 연속형 변수로 치환을 해주기 때문에 즉, 라벨을 고려해서 변환했기 때문에 모델의 성능을 높일 수 있다. 단점은 온전한 기존 정보가 손실이 어느정도 일어날 것이다. 위의 그림을 보면 만약에 A가 200, B도 200이라면 A와 B가 같은 값으로 인식하게 되어 정보 손실이 일어날 수도 있다.

 

결론: 상태 공간의 크기가 작으면 더미화, 상태 공간의 크기가 크면 연속형 변수로 치환하는 방법을 사용한다.

 


● 관련 문법: Series.unique()

Series에 포함된 유니크한 값을 반환해주는 함수로, 상태 공간을 확인하는데 사용한다.

 

● 관련 문법: feature_engine.categorical_encoders.OneHotCategoricalEncoder

변경:feature_engine.encoding.OneHotEncoder 

더미화를 하기 위한 함수로, 활용 방법은 sklearn의 인스턴스의 활용 방법과 유사하다.

 

-주요 입력

▷variables: 더미화 대상이 되는 범주형 변수의 이름 목록(주의: 해당 변수는 반드시 str 타입이어야 함, 숫자로 된 범주형 변수인 경우 astype 함수를 사용해서 자료형을 str으로 바꾸고 사용해야 함) → variables에 리스트를 넣어줌

▷drop_last: 한 범주 변수로부터 만든 더미변수 가운데 마지막 더미변수를 제거할 지를 결정

▷top_categories: 한 범주 변수로부터 만든 더미변수 개수를 설정하며, 빈도 기준으로 자름(예를들어, 한 범주형 변수의 상태 공간이 100개일때 100개의 특징을 다 쓰기 어렵기 때문에 그 중에 빈도가 높은 상위 10개만 쓰는 방식이다.)

 

참고사항: pandas.get_dummies()는 이 함수보다 사용이 훨씬 간단하지만, 인스턴스로 저장되는 것이 아니기 때문에 학습 데이터와 새로 들어온 데이터를 똑같이 처리 할 수 없다.

학습 데이터에 포함된 범주형 변수를 처리한 방식으로 새로 들어온 데이터에 적용이 불가능하기 때문에, 실제적으로 활용이 어려워서 머신러닝 모델을 구축할때는 잘 쓰이지 않는다.

 


## 코드 실습 ##

import os
os.chdir(r"C:\Users\82102\Desktop\데이터전처리\머신러닝을 위한 필수 전처리\Part 4. 머신러닝을 위한 필수 전처리\데이터")

import pandas as pd
df= pd.read_csv("car-good.csv")
df.head()

# 특징과 라벨 분리
X = df.drop('Class', axis=1)
Y = df['Class']
# 학습 데이터와 평가 데이터 분리
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(X, Y)
y_train.value_counts()

# 문자 라벨을 숫자로 치환
y_train.replace({"negative":-1, "positive":1}, inplace=True)
y_test.replace({"negative":-1, "positive":1}, inplace=True)
x_train.head() # Buying, Maint, Lug_boot, Safety 변수가 범주형 변수로 판단됨

# 자세한 범주형 변수 판별 -> 모든 변수가 범주형임을 확인
for col in x_train.columns:
    print(col, x_train[col].unique(), len(x_train[col].unique()))

 

  • 더미화를 이용한 범주 변수 처리
x_train = x_train.astype(str) # 모든 변수가 범주형이므로, 더미화를 위해 전부 string 타입으로 변환
from feature_engine.encoding import OneHotEncoder as OHE

# 인스턴스화
dummy_model = OHE(variables=x_train.columns.tolist(),
                 drop_last=True) 
# 더미화 대상 컬럼은 모든 컬럼이기 때문에 x_train.columns를 입력
# variables로 list를 받음->tolist를 이용해서 리스트로 바꿈

# 학습(fit)
dummy_model.fit(x_train)

# transform
d_x_train = dummy_model.transform(x_train)
d_x_test = dummy_model.transform(x_test)
# 더미화를 한 뒤의 모델 테스트
from sklearn.neighbors import KNeighborsClassifier as KNN
model = KNN().fit(d_x_train, y_train)
pred_y = model.predict(d_x_test)

from sklearn.metrics import f1_score
f1_score(y_test, pred_y)

# 더미화를 해서 데이터가 sparse 해지는 경향이 있기 때문에
# 모델을 KNN을 써서 성능을 올리고 싶으면 metric으로 'jaccard'를 쓰면 좀 더 좋은 성능을 기대할 수 있다.
from sklearn.neighbors import KNeighborsClassifier as KNN
model = KNN(metric='jaccard').fit(d_x_train, y_train)
pred_y = model.predict(d_x_test)

from sklearn.metrics import f1_score
f1_score(y_test, pred_y)
# 성능이 조금 오름

 

  • 연속형 변수로 치환
# 라벨 정보를 활용하기 위해서 분할했던 데이터프레임 합침
train_df = pd.concat([x_train, y_train], axis=1)
train_df.head()

# x_train 컬럼들이 모두 범주형 변수이므로 x_train.columns를 for문에 사용
# 만약, 범주형이 아닌 변수가 있을 경우 분할해서 연속형 변수로 치환 후 합침
# x_train_cate = x_train[['범주형 변수'...]], x_test_cate = x_test[['범주형 변수'...]]
for col in x_train.columns:
    # col에 따른 Class의 평균을 나타내는 사전(replace를 쓰기 위해, 사전으로 만듦)
    temp_dict = train_df.groupby(col)['Class'].mean().to_dict()
    #print(temp_dict)
    
    #print('\n', '*'*100, '\n')
    
    # 변수 치환
    train_df[col] = train_df[col].replace(temp_dict)
    #print(train_df[col])
    
    #print('\n', '*'*100, '\n')
    
    # 테스트 데이터도 같이 치환해줘야 함
    x_test[col] = x_test[col].astype(str).replace(temp_dict)
    #print(x_test[col])
    
    #print('\n', '*'*100, '\n')
{'high': -1.0, 'low': -0.8375, 'med': -0.9090909090909091, 'vhigh': -1.0}

 **************************************************************************************************** 

276   -1.000000
859   -0.837500
766   -0.837500
51    -1.000000
708   -0.837500
         ...   
373   -1.000000
275   -1.000000
446   -0.909091
536   -0.909091
749   -0.837500
Name: Buying, Length: 648, dtype: float64

 **************************************************************************************************** 

65    -1.000000
436   -0.909091
814   -0.837500
229   -1.000000
682   -0.837500
         ...   
392   -1.000000
91    -1.000000
128   -1.000000
790   -0.837500
364   -1.000000
Name: Buying, Length: 216, dtype: float64

 **************************************************************************************************** 

{'high': -1.0, 'low': -0.7908496732026143, 'med': -0.9375, 'vhigh': -1.0}

 **************************************************************************************************** 

276   -1.00000
859   -0.79085
766   -0.93750
51    -1.00000
708   -1.00000
        ...   
373   -0.93750
275   -1.00000
446   -1.00000
536   -1.00000
749   -1.00000
Name: Maint, Length: 648, dtype: float64

 **************************************************************************************************** 

65    -1.00000
436   -1.00000
814   -0.79085
229   -1.00000
682   -1.00000
        ...   
392   -0.79085
91    -1.00000
128   -0.93750
790   -0.93750
364   -0.93750
Name: Maint, Length: 216, dtype: float64

 **************************************************************************************************** 

{'2': -0.9345794392523364, '3': -0.9285714285714286, '4': -0.9428571428571428}

 **************************************************************************************************** 

276   -0.934579
859   -0.942857
766   -0.934579
51    -0.942857
708   -0.934579
         ...   
373   -0.942857
275   -0.934579
446   -0.934579
536   -0.942857
749   -0.942857
Name: Doors, Length: 648, dtype: float64

 **************************************************************************************************** 

65    -0.934579
436   -0.934579
814   -0.934579
229   -0.934579
682   -0.928571
         ...   
392   -0.934579
91    -0.942857
128   -0.928571
790   -0.928571
364   -0.942857
Name: Doors, Length: 216, dtype: float64

 **************************************************************************************************** 

{'2': -1.0, '4': -0.8679245283018868}

 **************************************************************************************************** 

276   -1.000000
859   -0.867925
766   -0.867925
51    -0.867925
708   -1.000000
         ...   
373   -0.867925
275   -1.000000
446   -0.867925
536   -0.867925
749   -0.867925
Name: Persons, Length: 648, dtype: float64

 **************************************************************************************************** 

65    -0.867925
436   -1.000000
814   -1.000000
229   -0.867925
682   -0.867925
         ...   
392   -0.867925
91    -1.000000
128   -1.000000
790   -0.867925
364   -1.000000
Name: Persons, Length: 216, dtype: float64

 **************************************************************************************************** 

{'big': -0.9444444444444444, 'med': -0.9369369369369369, 'small': -0.9238095238095239}

 **************************************************************************************************** 

276   -0.944444
859   -0.936937
766   -0.923810
51    -0.944444
708   -0.944444
         ...   
373   -0.936937
275   -0.936937
446   -0.936937
536   -0.936937
749   -0.923810
Name: Lug_boot, Length: 648, dtype: float64

 **************************************************************************************************** 

65    -0.923810
436   -0.936937
814   -0.936937
229   -0.936937
682   -0.944444
         ...   
392   -0.936937
91    -0.923810
128   -0.923810
790   -0.944444
364   -0.936937
Name: Lug_boot, Length: 216, dtype: float64

 **************************************************************************************************** 

{'high': -0.8823529411764706, 'low': -1.0, 'med': -0.9252336448598131}

 **************************************************************************************************** 

276   -1.000000
859   -0.925234
766   -0.925234
51    -1.000000
708   -1.000000
         ...   
373   -0.925234
275   -0.882353
446   -0.882353
536   -0.882353
749   -0.882353
Name: Safety, Length: 648, dtype: float64

 **************************************************************************************************** 

65    -0.882353
436   -0.925234
814   -0.925234
229   -0.925234
682   -0.925234
         ...   
392   -0.882353
91    -0.925234
128   -0.882353
790   -0.925234
364   -0.925234
Name: Safety, Length: 216, dtype: float64

 **************************************************************************************************** 
# 잘 바뀜
train_df.head()

# 모델링을 위해 학습 데이터를 다시 라벨과 분리
x_train = train_df.drop('Class', axis=1)
y_train = train_df['Class']
# 연속형 변수로 치환한 뒤의 모델 테스트
model = KNN().fit(x_train, y_train)
pred_y = model.predict(x_test)

f1_score(y_test, pred_y)
# 라벨을 고려한 전처리이므로 더미화보다 좋은 결과가 나온 것을 확인
# 차원도 줄고 모델의 성능이 더 좋아짐

●결측치 예측 모델 정의

결측이 발생하지 않은 컬럼을 바탕으로 결측치를 예측하는 모델을 학습하고 활용하는 방법이다.

쉽게 생각해서, 결측치를 추정해야 할 컬럼을 새로운 라벨로 보고 다른 컬럼들을 특징으로 봐서 모델을 학습하고 그 모델의 결과를 바탕으로 결측치를 대체한다.

 

●결측치 예측 모델 활용 방법

결측치 예측 모델은 어느 상황에서도 무난하게 활용할 수 있으나, 사용 조건 및 단점을 반드시 숙지해야 한다.

 

사용조건 1. 결측이 소수 컬럼에 쏠리면 안된다.

소수 컬럼의 결측 비율이 60프로 이상이면 사용하기 어렵다. 왜냐하면 결측치를 추정해야 할 컬럼을 새로운 라벨로 보는데 그 라벨값 자체가 부족하기 때문에 일반화된 모델을 만들기 어렵기 때문이다. 

하지만 결측치를 추정해야 할 컬럼에 포함된 레코드의 갯수가 충분히 많으면 사용이 가능하다.

예를 들어, 어떤 컬럼에 결측률이 90프로라고 하더라도 남은 10프로가 충분히 많은 경우 사용이 가능하다.

 

사용조건 2. 특징간에 상관관계가 존재해야 한다.

 

단점. 모델을 만들어야 하기 때문에 다른 결측치 처리 방법에 비해 시간이 오래 소요된다.

 

●관련 문법: sklearn.impute.KNNImputer

KNN 알고리즘을 사용하여 결측치를 채우는 방식으로 결측이 아닌 값만 사용하여 이웃을 구한 뒤, 이웃들의 값의 대표값으로 결측을 대체하는 결측치 예측 모델이다. 

 

KNNImputer는 숫자로만 계산이 가능하기 때문에 범주형 변수에 대해서는 더미화(가변수화, 데이터 인코딩) 이후 사용해야한다.

 

-주요 입력

▷n_neighbors: 이웃 수(너무 적으면 결측 대체가 정상적으로 이루어지지 않을 수 있으므로 5 정도가 적절하다.)

(transform 한 후 표에서 c에서 V1값은 2이다. 표 오류)

(KNNImputer 파라미터의 거리척도의 default값은 'nan_euclidean'으로 유클리디안 거리 척도를 사용한다.)

 

 

## 코드 실습 ##

import os
os.chdir(r"C:\Users\82102\Desktop\데이터전처리\머신러닝을 위한 필수 전처리\Part 4. 머신러닝을 위한 필수 전처리\데이터")

import pandas as pd
df = pd.read_csv("mammographic.csv")
df.head()

X = df.drop('Output', axis=1) # 결과가 2차원, 열에 대한 연산 -> axis=1
Y = df['Output']
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(X, Y)
# 열별 결측치 확인
x_train.isnull().sum(axis=0)

# 결측치를 구체적으로 알아보기 위해 열별 결측치 비율 확인
x_train.isnull().sum(axis=0) / len(x_train)
# 결측이 소수 컬럼에 쏠리지 않음, 결측률이 높지 않음을 확인

# 특징 간 상관관계 확인 -> 평균적으로 40~50%로 높음을 확인 -> 특징 간 상관관계 존재
x_train.corr().sum() / len(x_train.columns)

결측치 모델을 사용하기에 적합한 조건이다.

# KNNImputer 인스턴스화
from sklearn.impute import KNNImputer
KI = KNNImputer(n_neighbors=5)

# KNNImputer 학습
KI.fit(x_train)

# 결측 대체: instance의 output은 ndarray이므로 DataFrame으로 변환
x_train = pd.DataFrame(KI.transform(x_train), columns=x_train.columns)
x_test = pd.DataFrame(KI.transform(x_test), columns=x_test.columns)
x_train.isnull().sum() / len(x_train)

x_test.isnull().sum() / len(x_test)

 

 

근처값으로 대체(시계열 변수에 한해서 사용 가능)

시계열 변수인 경우에는 결측이 바로 이전 값 혹은 이후 값과 유사할 가능성이 높기 때문에 근처값으로 대체하는 경우가 있다.

 

  • 관련 문법: DataFrame.fillna

결측치를 특정 값이나 방법으로 채우는 함수

-주요 입력

▷value: 결측치를 대체할 값

▷method: 결측치를 대체할 방법

       -ffill: 결측치 이전의 유효한 값 가운데 가장 가까운 값으로 채움

       -bfill: 결측치 이후의 유효한 값 가운데 가장 가까운 값으로 채움

보통은 ffill로 이전의 값으로 채워주고 안되는 경우 한해서 bfill로 채워준다.

주의해야 할점은 인덱스가 반드시 시간에 따라서 정렬되야한다.(만약에 시간을 섞어놓고 바로 앞에 값으로 채우는 것은 맞지 않는 방법이다.) 

 

 

## 코드 실습 ## (시계열 결측치 대체하기)

데이터 분할하기 전에 결측치 대체가 가능한 유일한 케이스이다. 왜냐하면 평가 데이터에 대한 결측을 채우게 되면 평가 데이터 내에서만 알아서 결측값이 채워지고, 마찬가지로 학습 데이터에 대한 결측도 학습 데이터 내에서만 알아서 결측이 채워지기 때문이다. 즉, 학습 데이터에서 결측이 있으면 어떤 값을 채워라고 학습된 내용이 아니기 때문이다.

 

데이터를 train_test_split을 이용하여 임의로 분할한 경우에는 적용이 불가능하다. 왜냐하면 train_test_split을 사용하면 시간 순서가 꼬이기 때문이다.

import os
os.chdir(r"C:\Users\82102\Desktop\데이터전처리\머신러닝을 위한 필수 전처리\Part 4. 머신러닝을 위한 필수 전처리\데이터")

import pandas as pd
df = pd.read_excel("AirQuality.xlsx")
df.head() # 시계열 데이터

df.isnull().sum(axis=0)

df = df.fillna(method='ffill').fillna(method='bfill')
# 데이터에 target 값도 없고, train_test_split을 사용하면 순서가 꼬이기 때문에 원본 그대로에 대해서 fillna로 채움
# 먼저 ffill로 이전값으로 채우고, 맨앞에 있는 값들은 안채워질테니 bfill로 채운다.
df.isnull().sum()

시계열 결측치 대체를 할 때 한 변수에 결측이 많다면 이전 값을 채운다고 했을 때 그 이전 값이 굉장히 멀리 떨어진 값일 수도 있다. 이런 경우에는 근처값을 사용해서 시계열 결측치 대체를 할 수 없다.

특히 한 컬럼에 결측이 연속해서 많이 쏠린게 있다면 np.cumsum 함수를 응용해 연속으로 얼마나 결측이 발생했는지 사전에 체크한 다음 사용하는 것이 바람직하다.

 

대표값으로 대체(SimpleImpute)

가장 널리 사용되는 방법이지만, (1) 소수 특징에 결측이 쏠린 경우와 (2) 특징 간 상관성이 큰 경우에는 사용하기 부적절하다.

일반적으로 대표값으로 연속형 변수는 평균, 중위수 등 범주형 변수는 최빈값 등을 주로 사용한다. V1이 범주형 변수이고 최빈값을 뽑아야 되는 상황이라 하면 V1에는 1이라는 값 밖에 없기 때문에 V1의 최빈값은 1이다. 그래서 결측을 전부 1로 대체하면 값이 1 한 개밖에 없는데 1이 실제로 V1을 대표할 수 있는 값인지 모른다. 그리고 1로 다 대체를 하게되면 전부 같은 값을 갖게 되기때문에 변수로서 기능을 상실할 위험이 있다.

→소수 특징에 결측이 쏠린 경우 대표값을 대체하는 것은 문제가 있다.

 

V1에서 0이 5개 1이 4개 있으니 대표값 0으로 대체, V2에서 0이 4개 1이 5개 있으니 대표값 1로 대체한다. 그런데 V1이 0이면 V2가 1이고, V1이 1이면 V2는 0이라는 관계가 명확히 보인다. 즉, V1+V2=1이라는 명확한 관계가 있는데도 무시하고 V1에 0을 넣고 V2에 1을 넣은 것이다.

→대표값으로 대체하는 경우 특징 간 영향을 무시하기 때문에 특징 간 상관성이 큰 경우에는 부적절하다.

 

 

 

(Tip) sklearn을 이용한 전처리 모델(sklearn을 이용한 모든 전처리에 포함되는 내용이니 중요함)

sklearn을 이용한 대부분의 전처리 모델의 활용 과정의 이해는 매우 중요하며, 특히 평가 데이터는 전처리 모델을 학습하는데 사용하지 않음에 주목해야 한다.

step 1) 전처리 모델을 인스턴스화 시킨다.

인스턴스화 시킨게 Preprocessing model이다.

step 2) Preprocessing model을 Train data를 갖고 fitting을 시킨다.

그러면 Preprocessing model은 Train data를 어떻게 바꿔야 하구나 라는 정보를 기억하게 된다.

예를 들어서 각변수에 결측이 있는데 이 결측값을 뭘로 대체해야 하는지 (각 변수별로 대표값이 어떤건지) 알게 된다.

step 3) 그리고 Preprocessing model을 갖고 transform을 하면 Train data의 결측값들이 모델에 의해 추정된 대표값들로 채워지게 될 것이다. (preprocessed train data 생성)

또는 fit&trainsform을 한꺼번에 하는 함수로 원샷에 처리도 가능하다.

step 4) 그런 다음에 test data를 Preprocessing model로 transform을 해서 preprocessed test data를 생성한다.

또는 데이터를 train&test data를 분할하기전에 전체 데이터에 대해서 Preprocessing model을 갖고 fit&transform을 할수도 있다. 그러나 이런 경우에는 test data를 치팅한 효과가 나타나기 때문에 좋은 방법은 아니다.

주의해야할점은 전처리를 하던 모델링을 하던 우리가 다루는 데이터는 train data에 불과하다.

 

 

  • 관련 문법: sklearn.impute.SimpleImputer

결측이 있는 변수의 대표값으로 결측을 대체하는 인스턴스이다.

 

-주요 입력

▷strategy: 대표 통계량을 지정('mean', 'most_frequent', 'median')

변수 타입에 따라 두 개의 인스턴스를 같이 적용해야 할 수 있다. 만약 변수 타입이 혼재가 되어 있을 경우(연속형 변수에 대해서는 mean이나 median을 쓰는게 적합하고, 범주형 변수에 대해서는 most_frequent를 쓰는게 적합)데이터 프레임을 나눈 다음, 각각을 적용시킨후, 다시 열 단위로 이어 붙힌다.

 

 

## 코드 실습 ##

  • 모든 특징의 타입이 같은 경우
import os
os.chdir(r"C:\Users\82102\Desktop\데이터전처리\머신러닝을 위한 필수 전처리\Part 4. 머신러닝을 위한 필수 전처리\데이터")

import pandas as pd
df = pd.read_csv("cleveland.csv")
df.head()

X = df.drop('Output', axis=1)
Y = df['Output']
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(X, Y)
x_train.isnull().sum(axis=0)

결측치가 많이 없어서 열 단위 결측 삭제 하기에는 적합하지 않지만 행 단위 결측 삭제 하기에는 적합해 보인다.

가정1: 도메인 지식에 의해 새로 들어온 데이터에 결측이 있을 수도 있다고 가정

→가정 1에 의해 학습 데이터에서 결측을 지웠을 때 새로 들어온 데이터도 결측을 지우는 방법이 있지만 결측값을 대표값으로 대체하기로 결정했다.

 

# 평균 상관계수 확인(주의: 모든 변수가 연속형이므로 가능한 접근)
x_train.corr().sum() / (len(x_train.columns)-1)

 

열 별 상관계수의 합을 구한뒤 (열 개수 -1)로 나누어준다. 여기서 1을 뺀 이유는 대각행렬의 값은 모두 1이기 때문이다.

(대각행렬을 빼고 평균 상관계수를 구할거면 (x_train.corr().sum()-1) / (len(x_train.columns)-1) 코드, 대각행렬을 포함해서 평균 상관계수를 구할거면 (x_train.corr().sum) / (len(x_train.columns)코드가 맞다고 생각한다.)

 

→상관계수가 높지 않기 때문에 특징 간 관계가 크지 않다고 판단한다. 그래서 결측을 대표값으로 대체 가능하다고 판단한다.

# 대표값을 활용한 결측치 대체
from sklearn.impute import SimpleImputer

# SimpleImputer 인스턴스화
SI = SimpleImputer(strategy='mean')

# 학습
# fit을 하면 위에 Ca 변수의 대표값과 Thal 변수의 대표값을 계산할 수 있게 된다.
# 엄밀하게는 모든 피쳐에 대해 mean을 계산해서 기억하고 있을 것이다.
SI.fit(x_train)

# sklearn instance의 출력은 ndarray이므로 다시 DataFrame으로 바꿔줌
x_train = pd.DataFrame(SI.transform(x_train), columns=x_train.columns)
x_test = pd.DataFrame(SI.transform(x_test), columns=x_test.columns)
x_train.isnull().sum(axis=0)

 

  • 다른 타입의 특징이 있는 경우
df = pd.read_csv("saheart.csv")
df.head()

X = df.drop('Chd', axis=1)
Y = df['Chd']
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(X, Y)
x_train.isnull().sum(axis=0)
# 결측치가 많지 않음

결측치가 많이 없어서 열 단위 결측 삭제 하기에는 적합하지 않지만 행 단위 결측 삭제 하기에는 적합해 보인다.

가정1: 도메인 지식에 의해 새로 들어온 데이터에 결측이 있을 수도 있다고 가정

→가정 1에 의해 학습 데이터에서 결측을 지웠을 때 새로 들어온 데이터도 결측을 지우는 방법이 있지만 결측값을 대표값으로 대체하기로 결정했다.

# 평균 상관 계수 확인 (주의: 모든 변수가 연속형이므로 가능한 접근)
x_train.corr().sum() / (len(x_train.columns)-1)

Adipostiy 변수의 상관계수가 높지만 실습을 위해서 높지 않다고 가정하고 진행한다. 사실 엄밀하게 따지면 위에서 나온 상관계수는 틀린것이다. 데이터가 연속형과 범주형이 섞여있기 때문에 통계분석(범주형일 경우 Anova 검정을 하거나 연속형일 경우 피어슨 상관계수를 구한다던가 하는 방식)으로 이 변수들간에 관계성을 구해야 하는게 맞다.

 

"saheart.csv" 데이터 설명서를 보면 'Famhist'는 범주형 변수이고 나머지는 연속형 변수이다.

그래서 대표값을 평균을 사용할지, 최빈값을 사용할지 결정이 어렵기 때문에 결론은 변수의 형태에 따라 데이터를 분할해서 각각에 맞는 SimpleImputer를 진행해야 한다.

 

따라서 데이터를 아래와 같이 분할한다.

x_train_cate = x_train[['Famhist']] # 학습 데이터(범주형 변수)
x_train_cont = x_train.drop('Famhist', axis=1) # 학습 데이터(연속형 변수)

x_test_cate = x_test[['Famhist']] # 평가 데이터(범주형 변수)
x_test_cont = x_test.drop('Famhist', axis=1) # 평가 데이터(연속형 변수)

 

 

그후, SimpleImputer를 이용한 대표값을 활용한 결측치 대체한다.

# 대표값을 활용한 결측치 대체
from sklearn.impute import SimpleImputer

# SimpleImputer 인스턴스화
SI_mode = SimpleImputer(strategy='most_frequent')
SI_mean = SimpleImputer(strategy='mean')

# 학습
SI_mode.fit(x_train_cate)
SI_mean.fit(x_train_cont)

# sklearn instance의 출력은 ndarray이므로 다시 DataFrame으로 바꿔줌
x_train_cate = pd.DataFrame(SI_mode.transform(x_train_cate),
                           columns=x_train_cate.columns)

x_test_cate = pd.DataFrame(SI_mode.transform(x_test_cate),
                          columns=x_test_cate.columns)

x_train_cont = pd.DataFrame(SI_mean.transform(x_train_cont),
                           columns=x_train_cont.columns)

x_test_cont = pd.DataFrame(SI_mean.transform(x_test_cont),
                          columns=x_test_cont.columns)

# 두 데이터를 열단위로 이어붙힘
# 컬럼명이 있어야 하기 때문에 default값인 ignore_index=False 사용(default 값이니 명시는 안해줌)
x_train = pd.concat([x_train_cate, x_train_cont], axis=1)
x_test = pd.concat([x_test_cate, x_test_cont], axis=1)
x_train.isnull().sum(axis=0)

 

(Tip) 이진형 변수와 연속형 변수만 포함된 경우에는 SI_mean만 사용하여 결측치를 평균으로 대체한 뒤에, 이진형 변수에 대해서만 round(반올림) 처리를 하면 하나의 인스턴스만 활용할 수 있다. 

즉, 이진형 변수는 mean을 쓴다음에 거기에 round 처리를 해줘야 한다. 왜냐하면 이진형 변수의 평균이 결국에는 1의 비율과 같기 때문이다. 만약에 평균을 구했을때 0.4이면 0 이 더 많다는 소리이고, 0.7이다라고 하면 1이 더 많다는 얘기이다. 그러면 그 값을 바탕으로 round 시켜주면 0.5 이상이면 1로 바뀔테고 0.5미만이면 0으로 바뀔것이다.

이렇듯 위와 같이 범주형, 연속형 처럼 따로 전처리 모델을 학습할 필요가 없다.

 

행 단위 결측 삭제

행 단위 결측 삭제는 결측 레코드를 삭제하는 매우 간단한 방법이지만, 두 가지 조건을 만족하는 경우에만 수행할 수 있다.

첫 번째 조건의 결정 방법은 학습 샘플 개수에 따른 성능의 수렴 여부 확인이다. 예를 들어, 결측을 제거한 후 남은 샘플 개수가 1000개라고 하면 100개, 200개, 300개, 400개 등으로 모델을 만들어보고 만약 500개 지점부터 계속 비슷한 성능으로 수렴한다면 더 이상 데이터가 필요없다고 판단할 수 있고 결측을 제거한 남은 데이터가 모델을 만드는데 충분히 많다라고 판단할 수 있다.

두 번째 조건에서 새로운 데이터라는 것은 누구나 현재 시점에서 알 수가 없다. 새로운 데이터에 결측이 없을거라는 보장은 사실 누구도 하기 쉽지 않다. 그렇기 때문에 두 번째 조건은 반드시 도메인 지식을 활용해서 지울지 말지 판단해야한다.

 

열 단위 결측 삭제

열 단위 결측 삭제는 결측치를 포함하는 열을 삭제하는 매우 간단한 방법이지만, 두 가지 조건을 만족하는 경우에만 사용한다. 그리고 결측치를 포함하는 열을 지운다는 것은 새로 들어온 데이터에 그 해당 열에 결측이 있더라도 어차피 지우기 때문에 모델을 예측하는데는 큰 지장이 없다. 

 

첫 번째 조건: 소수 변수에 결측이 많이 포함되어 있는 경우

예를들어, 샘플의 개수가 10000개인데 한 컬럼에만 결측이 9000개가 있다라고 하면 행 단위 삭제를 하는 것은 바람직하지 않다.

두 번째 조건: 결측치를 포함하는 변수가 크게 중요하지 않은 경우(by 도메인 지식)

예를들어, 설비 부품의 고장을 예측하는 문제가 있는데, 도메인 지식을 통해 부품의 고장을 예측하는데 제일 잘 쓰이는 변수가 진동수라는 변수로 알고있다. 그런데 온도, 유압, 습도 이런 변수는 결측이 거의 없었는데 유독 진동수 변수에만 결측이 많았다. 일반적으로 한 컬럼에 30, 40% 정도의 결측치가 있다하면 그 컬럼을 지우는것이 더 좋을 것이라고 생각한다. 하지만 진동수가 도메인 지식하에 중요한 변수이므로 결측이 있는 행을 삭제하던가 원래 결측값들이 무엇이었는지 예측(결측 예측 모델)하는 등 다른 방법으로 진행한다. 

 


  • 관련 문법: Series / DataFrame.isnull

값이 결측이면 True를, 그렇지 않으면 False를 반환하고 sum 함수와 같이 사용하여 결측치 분포를 확인하는데 주로 사용한다.

 

  • 관련 문법: DataFrame.dropna

결측치가 포함된 행이나 열을 제거하는데 사용한다.

-주요 입력

▷axis: 1이면 결측이 포함된 열을 삭제하며, 0이면 결측이 포함된 행을 삭제한다.

 

 

## 코드 실습 ##

주의점: 결측을 제거하던 어떤 범주형 변수를 처리하던 학습 데이터를 기준으로 한다.(전처리 할 시 학습 데이터를 기준으로 한다) 

import os
os.chdir(r"C:\Users\82102\Desktop\데이터전처리\머신러닝을 위한 필수 전처리\Part 4. 머신러닝을 위한 필수 전처리\데이터")

import pandas as pd
  • 행 단위 결측 삭제
df = pd.read_csv("mammographic.csv")
df.head()

X = df.drop('Output', axis=1)
Y = df['Output']
from sklearn.model_selection import train_test_split
x_train,x_test, y_train, y_test = train_test_split(X, Y)
x_train.isnull().sum(axis=0)
# 수치만 봤을 때 얼마나 많은 결측인지 알 수 없으니 비율 확인

x_train.isnull().sum(axis=0) / len(x_train) # 열별 결측치 비율 확인
# 결측이 전체적으로 많은 편이 아니며 하나의 변수에 결측이 몰렸다고 보기 힘듦
# (Density가 다른 값들에 비해서 많기는 하지만 집중적으로 몰렸다고 보기에는 어려운 수치임)
# 모든 컬럼에 결측이 1회 이상 발생했기 때문에 열 삭제는 불가함

x_train.dropna(axis=0, inplace=True)
x_test.dropna(axis=0, inplace=True) # 새로 들어오는 데이터에는 결측이 없어야 함

 

  • 열 단위 결측 삭제
df = pd.read_csv("post_operative.csv")
df.head()# COMFORT 변수에 '?'로 결측이 표시되어 있음을 확인

X = df.drop('Decision', axis=1)
Y = df['Decision']
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(X, Y)
x_train.head() # COMFORT 변수에 '?'로 결측이 표시되어 있음을 확인
# 지금은 우연히 head()를 찍었을 때 '?'를 확인할수 있지만 '?'가 있지만 head()를 찍었을 때 안나올 수도 있음
# 각 컬럼들의 유니크한 값을 찍어보는 것이 좋다.

for i in x_train.columns:
    print(x_train[i].unique())

처음에 x_train.replace('?', np.nan, inplace=True).isnull().sum(axis=0) / len(x_train) 코드를 바로 실행하려고 했는데 

'nonetype' object has no attribute 'isnull' 에러가 발생했다. 구글에 이 에러에 대해서 알아보아도 잘 모르겠어서 

x_train.replace('?', np.nan, inplace=True)로 x_train 데이터프레임에서 먼저 '?'를 NaN값으로 바꾼 것을 inplace를 통해 x_train 다시 저장한 다음 그 x_train으로 isnull().sum(axis=0) / len(x_train)으로 결측의 비율을 확인했다. (에러가 발생한 이유를 아는 사람을 찾아서 왜 그런건지 꼭 알고싶다...)

# '?'를 결측으로 변환
import numpy as np
x_train.replace('?', np.nan, inplace=True)
# x_train.replace({'?':np.nan}).isnull().sum()
x_train.isnull().sum(axis=0) / len(x_train)

# 다른 변수에는 결측이 전혀 없고, COMFORT 변수에만 결측 비율이 높음

# 모든 결측이 COMFORT에 쏠렸으며, 해당 변수가 중요하지 않다는 도메인 지식 기반 하에 삭제
x_train.dropna(axis=1, inplace=True)

x_test.drop('COMFORT', axis=1, inplace=True) # x_test에는 COMFORT가 결측이 없었을 수도 있으므로, drop을 이용하여 삭제
# x_test = x_test[x_train.columns]

 

데이터에 결측치가 있어 모델 학습 자체가 되지 않는 문제로 결측치는 크게 NaN과 None으로 구분된다.

▷NaN: 값이 있어야 하는데 없는 결측으로 대체, 추정, 예측 등으로 처리한다.

▷None: 값이 없는게 값인 결측(e.g., 직업-백수)으로 새로운 값으로 정의하는 방식으로 처리한다.

 

예를들어, 어떤 survey에서 직업이 무엇인지 물어보는 문항이 있을 때 백수의 경우 직업이 없기 때문에 이 문항에 답을 할 수가 없다. 그래서 문항에 대한 답을 쓰지 않았을 것이다. 이처럼 백수일 경우에 애초에 직업이라는 값이 없는게 정상이다.

이런 경우 새로운 값으로 정의하는 방식으로 처리한다.

→None 값은 이정도로만 기억하고 결측치라고 하면  NaN 값이라고 생각하고 공부하면된다.

 

결측치 처리 방법 자체는 매우 간단하나 상황에 따른 처리 방법 선택이 매우 중요하다. 상황에 따른 처리 방법은 다음 챕터 부터 공부할것이다.

 

 

결측 레코드: 결측치를 포함하는 레코드

결측치 비율: 결측 레코드 수 / 전체 레코드 개수

 

 

 

+ Recent posts