결측치가 있으면 모델 성능에 안좋은 영향을 줄 수 있다는 것, 다들 아시죠?
그렇다면 당연히 이 결측치를 그대로 둘 수 없는데요,
데이터를 다루는 우리는 이러한 결측치들을 상황에 맞게 잘 처리할 수 있어야 할 것입니다.
이번 포스팅에서는 결측 데이터를 처리하는 방법에 대해 알아보겠습니다.
캐글의 학생 성적 예측 데이터셋을 이용해서 실제 코드와 함께 결측치 처리 실습도 진행해보겠습니다.
결측 데이터 처리 방법
결측치 처리 방법은 수치형 변수일 때와 범주형 변수일 때 다른데요, 각각의 방식은 아래와 같습니다.
방법 | 수치형 변수 | 범주형 변수 |
삭제 | 행 또는 열 삭제 → 유효한 데이터가 삭제될 위험 존재 | |
단순대체 | 평균, 중앙값, 최빈값 | 최빈값 |
고급대체 | KNN, 회귀모델 | KNN(하기 전 범주를 라벨인코딩으로 수치화) |
보간 | 선형 보간법(interpolate), 시계열 기반 대체 | 상위 또는 하위 데이터로 대체(forward/backward) |
이번 포스팅에서는 이 중에서도 자주 사용되는 단순대체와 고급대체 기법을 실습해보도록 하겠습니다!
결측 데이터 처리 실습
데이터셋 정보
데이터셋은 캐글에서 가져온 학생 성적 예측 데이터셋입니다!
https://www.kaggle.com/datasets/ishandutta/student-performance-data-set
Student Performance Data Set
Student Performance Data Set
www.kaggle.com
☞ 종속 변수 : G3(최종 성적)
☞ 데이터 크기 : 678행 X 33열
☞ 수치형(int) 변수 16개, 범주형(object) 변수 17개
- school: 학생의 학교 (이진형: 'GP' - Gabriel Pereira 또는 'MS' - Mousinho da Silveira)
- sex: 학생의 성별
- age: 학생의 나이
- address: 학생의 거주지 유형 (이진형: 'U' - 도시 또는 'R' - 시골)
- famsize: 가족 구성원 수 (이진형: 'LE3' - 3명 이하 또는 'GT3' - 3명 이상)
- Pstatus: 부모의 동거 상태 (이진형: 'T' - 함께 거주 또는 'A' - 별거)
- Medu: 어머니의 교육 수준
- Fedu: 아버지의 교육 수준
- Mjob: 어머니의 직업
- Fjob: 아버지의 직업
- reason: 이 학교를 선택한 이유
- guardian: 학생의 보호자
- traveltime: 집에서 학교까지의 통학 시간
- studytime: 주간 학습 시간
- failures: 과거 수업 낙제 횟수
- schoolsup: 추가 교육 지원 여부
- famsup: 가족 교육 지원 여부
- paid: 교과 과목 관련 추가 유료 수업 여부 (이진형: 예 또는 아니요)
- activities: 방과 후 활동 참여 여부 (이진형: 예 또는 아니요)
- nursery: 유치원 교육 여부 (이진형: 예 또는 아니요)
- higher: 고등 교육 이수 의향 여부 (이진형: 예 또는 아니요)
- internet: 가정 내 인터넷 사용 가능 여부 (이진형: 예 또는 아니요)
- romantic: 연애 관계 여부 (이진형: 예 또는 아니요)
- famrel: 가족 관계의 질 (숫자형: 1 - 매우 나쁨에서 5 - 매우 좋음)
- freetime: 학교 후 여가 시간 (숫자형: 1 - 매우 적음에서 5 - 매우 많음)
- goout: 친구들과 외출 빈도 (숫자형: 1 - 매우 적음에서 5 - 매우 많음)
- Dalc: 평일 음주 빈도 (숫자형: 1 - 매우 적음에서 5 - 매우 많음)
- Walc: 주말 음주 빈도 (숫자형: 1 - 매우 적음에서 5 - 매우 많음)
- health: 현재 건강 상태 (숫자형: 1 - 매우 나쁨에서 5 - 매우 좋음)
- absences: 결석 횟수 (숫자형: 0에서 93까지)
- G1: 첫 번째 학기 점수 (숫자형: 0에서 20까지)
- G2: 두 번째 학기 점수 (숫자형: 0에서 20까지)
- G3: 최종 점수 (숫자형: 0에서 20까지, 종속 변수)
결측치 생성
이 데이터셋에는 결측치가 존재하지 않으므로, 결측치를 랜덤으로 생성합니다.
num_missing = 5000 # 결측값 갯수 정하기
# 랜덤 시드 고정
np.random.seed(42)
# 결측값 랜덤으로 주기
col = x_train.columns.drop('StudentID')
missing_indices = np.random.choice(x_train.index, size=num_missing, replace=True)
missing_features = np.random.choice(col, size=num_missing, replace=True)
# 결측값이 존재하는 데이터로 만들어주기
x_train_nan = x_train.copy()
for idx, feature in zip(missing_indices, missing_features):
x_train_nan.loc[idx, feature] = np.nan
결측치가 잘 생성되었는지 시각화를 한번 해볼까요?
plt.figure(figsize=(20,3))
sns.barplot(x_train_missing_summary, palette='husl')
plt.xticks(rotation=45)
plt.title("Random Missing Data")
골고루 잘 생성되었군요!
이제 본격적으로 결측치 처리 단계에 들어가보도록 하죠!
처리해야할 수치형 변수, 범주형 변수 확인
# 실수형(float) 컬럼 추출 -> 원래 정수였으나, 결측치를 주면서 실수형으로 바뀜
float_columns = x_train_nan.select_dtypes(include=['float64']).columns
# 문자열(object) 컬럼 추출
object_columns = x_train_nan.select_dtypes(include=['object']).columns
# 출력
print("실수형 컬럼:")
print(float_columns)
print("\n문자열 컬럼:")
print(object_columns)
A. 수치형 컬럼 전처리
- 중앙값 대체
- 평균값 대체(수치형 컬럼 평균값 대체 후 int로 변환해줘야 함)
- KNN Imputer → 실제 데이터에 전처리 적용
# 1. 중앙값 대체
x_train_nan[float_columns[0]].fillna(x_train_nan[float_columns[0]].median(), inplace=False).astype('int')
# 2. 평균값 대체 (정수형이기 때문에 int로 변환)
x_train_nan[float_columns[0]].fillna(x_train_nan[float_columns[0]].mean(), inplace=False).astype('int')
# 3. KNN Imputer
from sklearn.impute import KNNImputer
df = x_train_nan.copy()
imputer = KNNImputer(n_neighbors=5) # 3~5 중 선택
df[float_columns] = imputer.fit_transform(df[float_columns])
df[float_columns] = df[float_columns].astype('int32')
# 결측치가 채워졌는지 확인
df[float_columns].info()
B. 범주형 컬럼 전처리
- 결측치 삭제 -> 유효한 데이터가 삭제될 위험있음
- 그룹별 최빈값 대체
- 유사한 레코드의 값으로 대체(KNN Imputer) → 실제 데이터에 전처리 적용
# 1. 결측치 삭제 -> 현재는 전체 컬럼에 결측치가 존재하도록 결측치를 많이 생성한 상황이므로 전체 데이터가 삭제됨.
# 지금과 같이 결측치가 많은 상황에서는 부적절한 방법이라고 볼 수 있음
x_train_nan.dropna()
# 2. 그룹별 최빈값 대체 - groupbyS
x_train_nan.groupby('age')['school'].transform(lambda x: x.fillna(x.mode()[0]))
# 3. KNN Imputer
# 결측값이 있는 문자열 컬럼을 가장 가까운 이웃의 값으로 대체
# 문자열은 수치로 변환한 뒤 적용
# df = x_train_nan.copy()
from sklearn.impute import KNNImputer
from sklearn.preprocessing import LabelEncoder
# 숫자 변환
label_encoders = {} # 각 컬럼별 LabelEncoder 저장
for col in object_columns: # 문자열 컬럼
le = LabelEncoder()
# 결측치 제외한 데이터만 LabelEncoder에 적용 -> 결측치를 채우기 위해, 결측치는 라벨로 인식하면 안됨.
non_nan_mask = df[col].notnull() # 결측치가 아닌 행 필터
df.loc[non_nan_mask, col + '_encoded'] = le.fit_transform(df.loc[non_nan_mask, col])
label_encoders[col] = le # 각 컬럼의 LabelEncoder 저장 -> 각 컬럼마다 label이 다르기 때문에 각각 저장해야 함
# KNN Imputer 객체 생성
knn_imputer = KNNImputer(n_neighbors=3)
# 결측값 대체
encoded_columns = [col for col in df.columns if '_encoded' in col]
df[encoded_columns] = knn_imputer.fit_transform(df[encoded_columns])
# 결측값 채우기 완료 확인
df[encoded_columns].info()
'Study > Data Science' 카테고리의 다른 글
[Data Science] Feature(차원) 축소 - 주성분 분석(PCA, Principal Component Analysis) (0) | 2024.12.07 |
---|---|
[Data Science] Feature Selection - 유전 알고리즘(Genetic Algorithm, GA) (0) | 2024.12.07 |
[Data Science] 이상치(Outlier) 탐지 방법 (1) | 2024.12.05 |
[Data Science] 크롤링 라이브러리 (파이썬) (1) | 2024.12.05 |