본문 바로가기
Data Science/Machine Learning

[Machine Learning] 데이터 전처리(1) - 결측치 처리(2)

by 인사이티드 2023. 6. 26.
 

[Machine Learning] 데이터 전처리(1) - 결측치 처리(1)

데이터 분석을 하기 위해서 데이터의 전처리는 필수적이다. 데이터들을 수집해서 나온 가공되지 않은 데이터는 분석을 바로 할 수가 없는 상태이다. (결측치의 존재, 이상치의 존재, 여러 데이

insighted-h.tistory.com


앞에서 결측치 처리 방법으로 크게 3가지 방법을 학습하였다. 그래서 실제로 얼마나 유의미한 차이가 있을까? 앞에서의 3가지 방법을 다음 데이터셋에 적용해서 비교해보았다.
https://www.kaggle.com/c/spaceship-titanic

 

Spaceship Titanic | Kaggle

 

www.kaggle.com


위 데이터셋은 승객들이 정상적으로 다른 차원의 공간으로 보내졌을지(보내졌다면 True, 아니라면 False) 예측하는 이진 분류 문제이다. 나는 train 데이터셋으로 각 방법에 따른 정확도를 평가해볼 것이다.
데이터는 특별한 전처리 없이 카테고리 변수들만 다시 인코딩해주었다. 사이킷런의 LabelEncoder()는 인코딩 시 결측치까지 인코딩하기 때문에 나중에 인코딩하였다.

train = pd.read_csv('/content/drive/MyDrive/ML_Data/kaggle/spaceship-titanic/train.csv')
train.shape
=>
(8693, 15)
train.info()
=>
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 8693 entries, 0 to 8692
Data columns (total 14 columns):
 #   Column        Non-Null Count  Dtype  
---  ------        --------------  -----  
 0   PassengerId   8693 non-null   object 
 1   HomePlanet    8492 non-null   object 
 2   CryoSleep     8476 non-null   object 
 3   Cabin         8494 non-null   object 
 4   Destination   8511 non-null   object 
 5   Age           8514 non-null   float64
 6   VIP           8490 non-null   object 
 7   RoomService   8512 non-null   float64
 8   FoodCourt     8510 non-null   float64
 9   ShoppingMall  8485 non-null   float64
 10  Spa           8510 non-null   float64
 11  VRDeck        8505 non-null   float64
 12  Name          8493 non-null   object 
 13  Transported   8693 non-null   bool   
dtypes: bool(1), float64(6), object(7)
memory usage: 891.5+ KB
train.isnull().sum()	# Null 데이터 확인
=>
PassengerId       0
HomePlanet      201
CryoSleep       217
Destination     182
Age             179
VIP             203
RoomService     181
FoodCourt       183
ShoppingMall    208
Spa             183
VRDeck          188
Name            200
Transported       0
Cabin_deck      199
Cabin_side      199
dtype: int64
train[['Cabin_deck', 'Cabin_num', 'Cabin_side']] = train.loc[:,'Cabin'].str.split('/', expand=True)
train = train.drop(['Cabin', 'Cabin_num'], axis=1)		# Cabin_num 변수는 유의한 차이가 없어서 drop
train['Transported'] = train['Transported'].map({False:0, True:1})

train_cat = train.select_dtypes(exclude=['number'])
cols = list(train_cat.columns)
cols.remove('Name')
cols.append('Age')
cols.append('Transported')
cols
=>
['PassengerId',
 'HomePlanet',
 'CryoSleep',
 'Destination',
 'VIP',
 'Cabin_deck',
 'Cabin_side',
 'Age',
 'Transported']
# 카테고리 변수 확인
features = ['HomePlanet', 'CryoSleep', 'Destination', 'VIP', 'Cabin_deck', 'Cabin_side']

for feature in features:
  print(feature)
  print("------------------------------------------")
  print(train[feature].value_counts())
 
=>
HomePlanet
------------------------------------------
Earth     4602
Europa    2131
Mars      1759
Name: HomePlanet, dtype: int64
CryoSleep
------------------------------------------
False    5439
True     3037
Name: CryoSleep, dtype: int64
Destination
------------------------------------------
TRAPPIST-1e      5915
55 Cancri e      1800
PSO J318.5-22     796
Name: Destination, dtype: int64
VIP
------------------------------------------
False    8291
True      199
Name: VIP, dtype: int64
Cabin_deck
------------------------------------------
F    2794
G    2559
E     876
B     779
C     747
D     478
A     256
T       5
Name: Cabin_deck, dtype: int64
Cabin_side
------------------------------------------
S    4288
P    4206
Name: Cabin_side, dtype: int64
train = train[cols]
train = train.drop('PassengerId', axis=1)

# Imputation 방법에 따른 복사 데이터셋 생성
train_imp1 = train.copy()
train_imp2 = train.copy()
train_imp3 = train.copy()

학습은 모두 동일하게 LogisticRegression(random_state=42)으로 학습하였고, 평가지표는 정확도(Accuracy)를 사용하였다.

 

1. Listwise Deletion

train_imp1 = train_imp1.dropna()
print(f"Original Data: {train.shape} => Listwise Deletion: {train_imp1.shape}")
=>
Original Data: (8693, 8) => Listwise Deletion: (7572, 8)
train_imp1 = train_imp1.reset_index()
train_imp1 = train_imp1.drop('index', axis=1)

X_train, y_train = train_imp1.drop(columns='Transported'), train_imp1['Transported']

features = ['HomePlanet', 'CryoSleep', 'Destination', 'VIP', 'Cabin_deck', 'Cabin_side']

for feature in features:
  le = LabelEncoder()
  le = le.fit(X_train[feature])
  X_train[feature] = le.transform(X_train[feature])
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_validate

model1 = LogisticRegression(random_state=42)
cv1 = cross_validate(model1, X_train, y_train, cv=5, scoring='accuracy')
round(cv1['test_score'].mean(), 4) * 100
=>
71.87

Listwise Deletion의 경우 71.87%의 정확도를 보였다.

 


2. Mean / Median imputation
카테고리컬 데이터들은 최빈값으로 대체하였다.

features = ['HomePlanet', 'CryoSleep', 'Destination', 'VIP', 'Cabin_deck', 'Cabin_side']

for feature in features:
  print(train_imp2[feature].mode())
print(round(train_imp2['Age'].mean()))
=>
0    Earth
Name: HomePlanet, dtype: object
0    False
Name: CryoSleep, dtype: object
0    TRAPPIST-1e
Name: Destination, dtype: object
0    False
Name: VIP, dtype: object
0    F
Name: Cabin_deck, dtype: object
0    S
Name: Cabin_side, dtype: object
29
for feature in features:
  mode = train_imp2[feature].mode()[0]
  null_idx = train_imp2[feature].isnull()
  train_imp2.loc[null_idx, feature] = mode

mean = round(train_imp2['Age'].mean())
train_imp2['Age'] = train_imp2['Age'].fillna(mean)

train_imp2.isnull().sum()
=>
HomePlanet     0
CryoSleep      0
Destination    0
VIP            0
Cabin_deck     0
Cabin_side     0
Age            0
Transported    0
dtype: int64
train_imp2.shape
=>
(8693, 8)
model2 = LogisticRegression(random_state=42)
cv2 = cross_validate(model2, X_train, y_train, cv=5, scoring='accuracy')
round(cv2['test_score'].mean(), 4) * 100
=>
71.6

Mean / Median imputation의 경우 71.6%의 정확도를 보였다.


3. Model-based Imputation
수많은 imputation 방법 중, 나는 miceforest 패키지의 MICE를 활용하였다. MICE(Multiple Imputation by Chained Equations)란 예측 모델을 기반으로 반복적으로 결측치를 대체하여 수렴하는 알고리즘이다. miceforest 패키지에서는 LightGBM 모델을 기반으로 알고리즘이 돌아간다고 한다. 예측 모델을 기반으로 결측치 대체를 수행하기 때문에 시간이 오래 걸린다.

!pip install miceforest
cats = train_imp3.select_dtypes(exclude='number').columns

for feature in cats:
  # miceforest를 사용하기 위해서는 변수타입이 category이거나 numeric이어야 한다.
  train_imp3[feature] = train_imp3[feature].astype('category')

train_imp3.info()
import time
import miceforest as mf

start_time = time.time()

kds = mf.ImputationKernel(
    train_imp3,
    random_state=42
)
kds.mice(5)
train_imp3 = kds.complete_data()

end_time = time.time()
t = round(end_time - start_time, 2)
print(f"{t}sec")
=>
20.3sec		# 앞의 두 예시들은 거의 즉각적으로 수행이 가능한 데에 반해 20초나 걸렸다.
X_train, y_train = train_imp3.drop(columns='Transported'), train_imp3['Transported']

features = ['HomePlanet', 'CryoSleep', 'Destination', 'VIP', 'Cabin_deck', 'Cabin_side']

for feature in features:
  le = LabelEncoder()
  le = le.fit(X_train[feature])
  X_train[feature] = le.transform(X_train[feature])

model3 = LogisticRegression(random_state=42)
cv3 = cross_validate(model3, X_train, y_train, cv=5, scoring='accuracy')
round(cv3['test_score'].mean(), 4) * 100
=>
72.00999999999999

MICE의 경우 72.01%의 정확도를 보였다.

결측치 대체 방법만을 바꿔주는 것만으로도 모델의 학습에 작지 않은 영향을 미치는 것을 확인할 수 있다.


참고자료

[1] https://pypi.org/project/miceforest/