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

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

▷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 값이라고 생각하고 공부하면된다.

 

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

 

 

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

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

 

 

 

데이터 요약이 포함되는 경우
보통 1:N 병합인 경우에 사용되며, 거래 데이터 및 로그 데이터와 병합하는 경우에 주로 사용된다.
해결 방안은 중복 레코드를 포함하는 데이터를 요약한 후 병합하는 방식으로 문제를 해결한다.

예를 들어, 거래 데이터에서 A라는 고객이 물건을 구매한 횟수가 여러번일때 A고객의 구매금액들의 통계량으로 요약 후 병합하는 방식이다.

  • 관련 문법: DataFrame.groupby()

조건부 통계량(조건에 따른 대상의 통계량)을 계산하기 위한 함수로 머신러닝 프로세스 뿐만 아니라, 통계 분석 등에서도 굉장히 자주 활용된다.
-주요 입력
▷by: 그룹화할 변수(컬럼명 혹은 컬럼명 리스트로 입력-리스트 형태로 입력)
▷as_index: 조건 변수를 index로 설정할 것인지 여부
-활용 예시
▷df.groupby(['성별'])['신장'].mean() # 성별(조건)에 따른 신장(대상)의 평균(통계량)



## 코드 실습 ##

# 경로 설정, 필요한 모듈 불러오기
import os
os.chdir(r"C:\Users\82102\Desktop\데이터전처리\머신러닝을 위한 필수 전처리\Part 4. 머신러닝을 위한 필수 전처리\데이터")

import pandas as pd

# 데이터 불러오기
demo_df = pd.read_csv("고객별_인구통계정보.csv", encoding='cp949')
purchasing_df = pd.read_csv("고객별_구매금액.csv", encoding='cp949')

# 데이터 요약(구매금액 합계)
purchasing_aggregated_df = purchasing_df.groupby(['고객ID'])['구매금액'].sum()

# 데이터 병합
merged_df = pd.merge(demo_df, purchasing_aggregated_df, left_on='고객ID', right_index=True)
merged_df.head()


거리 기반 병합이 필요한 경우
지역이 포함되는 문제에서 주소나 위치 변수 등을 기준으로 거리가 가까운 레코드 및 관련 통계치를 통합해야 하는 경우가 종종 있다.
이런 경우
첫째, 각 데이터에 포함된 레코드 간 거리를 나타내는 거리 행렬을 생성하고
둘째, 거리 행렬의 행 혹은 열 기준 최소값을 가지는 인덱스를 바탕으로 이웃을 탐색한 뒤
셋째, 이웃을 기존 데이터에 부착하는 방식으로 해결한다.

  • 관련 문법: scipy.spatial.distance.cdist

두 개의 행렬을 바탕으로 거리 행렬을 반환하는 함수이다.
-주요 입력
▷XA: 거리 행렬 계산 대상인 행렬(ndarray 및 DataFrame)로, 함수 출력의 행에 해당한다.
▷XB: 거리 행렬 계산 대상인 행렬(ndarray 및 DataFrame)로, 함수 출력의 열에 해당한다.
▷metric: 거리 척도('cityblock', 'correlation', 'cosine', 'euclidean', 'jaccard', 'matching' 등)

  • 관련 문법: ndarray.argsort()

작은 값부터 순서대로 데이터의 위치를 반환하는 함수로, 이웃을 찾는데 주로 활용되는 함수이다.
-주요 입력
▷axis

1차원 배열

1차원의 경우 axis 키워드가 작동하지 않는다.

2차원 배열



## 코드 실습 ##

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

import numpy as np
import pandas as pd
df1 = pd.read_csv("2019년_서울_아파트매매_실거래가.csv", encoding='cp949') # 데이터에 한글이 포함되기 때문에 'cp949'
df2 = pd.read_csv("2019년_서울시_아파트주소.csv", encoding='cp949')
df1.head()
df2.head()
# 데이터 병합(키 변수의 이름이 다르고, 키 변수로 여러개 사용)
df = pd.merge(df1, df2, left_on=['법정동', '도로명', '아파트'], right_on=['읍면동명', '도로명', '건물명'])
df3 = pd.read_excel("지하철역_위경도.xlsx")
df3.head()

df는 아파트의 위치(경위도)를 나타내는 데이터프레임 / df3는 지하철의 위치(경위도)를 나타내는 데이터프레임이다.
지금부터는 아파트의 위치로부터 가장 가까운 지하철역과 그 거리를 구해서 데이터프레임을 합치는 작업을 할 것이다.

# 거리 행렬을 만들기 위해 계산 대상인 행렬 만듬
df_location = df[['경도', '위도']]
df3_location = df3[['경도', '위도']]
# 거리 행렬 생성
from scipy.spatial.distance import cdist

dist_mat = cdist(XA=df_location, XB=df3_location, metric='cityblock')

거리 행렬에서 df_location(아파트)이 행이고 df3_location(지하철)이 열이니까 각각 아파트에 대해서 가장 가까운 지하철역의 위치를 가지고 올려면 axis를 1로 설정하고, [:,0]는 행은 전체, 열은 맨 앞에 나온 값들이 각 아파트 별로 가장 가까운 지하철의 index이다.

close_subway_index = dist_mat.argsort(axis=1)[:, 0]
df['가까운역'] = df3.iloc[close_subway_index]['역명'].values
# df3에서 close_subway_index의 인덱스 값들만 iloc로 가져와서 거기에 해당하는 역명만 가지고옴
# (tip) 새로운 시리즈를 만들 때는 list, ndarray를 사용하는 것이 바람직함

df['가까운역까지_거리'] = dist_mat[close_subway_index][:, 0]
df.head()


데이터를 병합하는 방식 중 거리 기반 병합이 필요한 경우를 살펴보았다. 이번 시간에 공부한 내용이 데이터 파편화 문제 1, 2, 3에서 공부한 내용보다 다소 생소하고 어렵게 느껴져서 여러번 다시 볼 예정이다.

+ Recent posts