데이터 전처리
데이터를 시각화하고 분석, 예측을 하기 위해 가장 많이 접하는 단어가 아마 "데이터 전처리" 일것입니다.
데이터 전처리란 데이터를 분석 또는 활용, 머신러닝 모델링을 수행하기 전에 원천 데이터를 정제하고 구조화하여 품질 높은 데이터 셋으로 만드는 일련의 과정으로 결측치를 처리하고, 이상값제거, 형식통일, 범주형변수 인코딩, 스케일링, 파생변수 생성등 데이터 활용 이전에 데이터를 정리하고 정형화하는 필수 단계 입니다.
데이터 분석과 활용에 있어서 전처리 과정이 아마 50%이상을 차지할 수도 있는 많이 시간과 과정이 필요한 작업이며, 전처리 결과에 따라 정확도와 신뢰도에 큰 영향을 준답니다.
전처리의 주요 단계는 아래와 같습니다. 원천데이터의 신뢰도에 맞게 아래의 과정을 순차적으로 진행하시면 됩니다.
1. 결측치 처리
2. 이상치 처리
3. 데이터 타입 변환
4. 범주형 변수 인코딩
5. 스케일링
6. 파생변수 생성
7.불필요한 컬럼 제거
데이터 전처리 테스트를 하기 위한 샘플 데이터는 공공데이터에서 제공하는 자료로 지역별 분양가 정보 csv 파일입니다. (아래에 첨부)
첫번째로, CSV 데이터를 Pandas 데이터프레임에 넣고, 샘플 데이터와, 데이터구조를 먼저 확인합니다.
import pandas as pd
import numpy as np
import openpyxl
df=pd.read_csv('C:\seoul_house_price.csv')
print(df.head())
print(df.info())
df.head() : 데이터 프레임의 샘플 데이터 (지역명, 규모구분, 연동, 월, 분양가격) 열에 대한 데이터 구조를 확인 수 있습니다.
df.info() : 데이터 구조를 확인 할 수 있습니다. 계획해둔 분석 방향과 데이터 타입이 일치하는지 검토하여야 합니다.
위의 데이터 구조를 확인해 보면 약간 특이한 점을 확인 할 수 있습니다. 분양가격열인데.. Object 타입을 가지고 있는 것을 확인 할 수 있으며, 아마 우리는 평균분양가, 최고가, 최저가,지역별 합계 등등 가격을 이용해 많은 계산을 할 것으로 보이는데 Object 타입이라면 불가능하겠지요. 아마 분양가격은 int64 타입으로 변경을 해야 할 것입니다.
1. 결측치 확인
데이터에 NaN 또는 Null , N/A,0 등의 값이 있는지를 확인하고 이러한 결측치 처리를 하여야 합니다. 처리하는 방법은 두가지가 있습니다 .
- 결측치 제거
- 결측치를 다른 값을 대체
자.. 우선 결측치가 있는지 확인해 볼까요?. 결측치 유무는 "전체데이터 건수- 각 컬럼 별 값이 있는 데이터 수"를 하면 컬럼 별 결측치 개수를 알 수 있습니다. 물론 위의 df.info()로 non-null 개수가 다른 열과 다름으로 판단 할 수도 있답니다. 다만 이때는 전체 데이터 건수가 명확하지 않기에 절차대로 한번 해볼게요.
print('전체데이터 건수:',len(df))
print('컬럼별 결측치 개수')
print(len(df)-df.count())
천체데이터와 천체데이터에서 값이 있는 데이터를 뺀 값을 출력하는 코드입니다. 아래와 같이 전체 데이터 4,505 와 컬럼별 데이터가 없는 값을 확인 할 수 있으며, 분양가격 컬럼의 295건은 Null 또는 NaN등 결측치로 판단할 수 있습니다.
다른 방법으로는 df.isnull().sum() 함수를 이용해서 동일한 결측치 값을 확인 할 수도 있습니다.
만약 사용하시는 데이터의 종류에 따라 0 의 값도 결측치 범주에 속한다고 정의가 필요하시면 사전에 0의 NaN으로 변경하시는게 좋습니다.
1-1. 결측값 처리 ,데이터 형 변환
이젠 결측치가 있다는 것을 확인하였으니 결측데이터를 어떻게 처리할지 정하여야 합니다.
- 결측 데이터 삭제
- 평균값, 중앙값으로 변경
- 최빈값으로 변경
- 특정 데이터로 변경
먼저 데이터 삭제 예시 입니다. 결측값이 존재하는 모든 열을 삭제하고 싶다면 Python의 dropna() 함수를 사용하면 됩니다.
데이터프레임 내의 모든 결측값을 삭제 하고 싶을 때는 df.dropan() 함수를 사용하시고, 특정 열 기준으로 결측값이 있는 행에 대한 삭제가 필요할 경우에는 df.dropna(subset=['컬럼명'])을 사용하시면 됩니다.
df=df.dropna(subset=['분양가격(㎡)'])
print(df.isnull().sum())
실행 시키면 아래와 같이 분양가격의 결측치는 0 이고, 총 행은 4,210건으로 분양가격의 결측치가 포함된 295행이 모두 삭제 된 것을 확인 할 수 있습니다.
결측값을 삭제하지 않고 원하는 대체값으로 변환을 원하시면 fillna('대체값') 함수를 이용하여 대체 하여야 합니다. 3가지 방법이 있으며 "평균값","최빈값","특정값" 으로 대체하는 방식으로 많이 진행됩니다.
먼저 mean()함수를 이용하여 해당열의 평균값을 구하는 예시입니다.
null_value=df['분양가격(㎡)'].mean()
위의 코드를 실행시키면... 에러가 나타납니다. 왜일까요? 처음 df.info() 로 구성을 확인하였을 때 '분양가격(㎡)'의 데이터 타입은 Object 였습니다. int형이 아니기에 수식함수가 실행 될 수 없겠지요.
그럼 먼저 데이터 타입부터 변경해야합니다. 데이터 형변환은 사실 간단한 작업이 될 수도 있지만, 대부분 많은 단계를 거쳐야 할 것입니다. Object 데이터 타입이기에, 결측치외에도 숫자형태로 변환될 수 없는 데이터 (예를들어 띄어쓰기, '-' 등등)이 있을 수 있으니 단계별로 데이터를 정리해야 합니다. 우선 그냥 한번 형변환을 해보겠습니다. 에러 메세지를 보면서 하나씩 처리를 해보도록 하죠.
df['분양가격(㎡)']=df['분양가격(㎡)'].astype('Int64')
형변환을 실행 시키면 ValueError: invalid literal for int() with base 10: ' ' 라고 에러메시지를 확인 하실 수 있습니다. 띄어 쓰기 두칸이 있다는 것으로 int형으로 변경이 안된다는 뜻입니다.
그럼 어떤 행들이 띄어쓰기 두칸으로 들어있는지 확인하고, int 형으로 변경하기 위해서는 0으로 값을 변경해야 합니다.
#띄어쓰기 두칸있는 값을 '0'으로 변경
df.loc[df['분양가격(㎡)']==' ','분양가격(㎡)']='0'
#띄어쓰기 두칸 있는 값이 있는지 다시 확인
print(df.loc[df['분양가격(㎡)']==' '])
여기서 중요한 점은 현재 분양가격 열은 Object 이기에 우리가 0으로 바꿔준다는 의미는 숫자 0이 아닌 문자로 인식되는 '0' 으로 변경해야 합니다.
자 그럼 다시 한번 int 로 형변환을 해볼까요?
df['분양가격(㎡)']=df['분양가격(㎡)'].astype('Int64')
세상사 쉽지 않다는게.. 또 에러가 나옵니다. 이번 에러 메시지는 cannot convert float NaN to integer 입니다. NaN 값이 있다는 것으로 값이 없는 행이 있다는 것입니다. int형에는 빈값이 존재하지 않기에 NaN은 fillna 함수를 이용해서 '0'으로 대체 해줘야 합니다.
df['분양가격(㎡)']=df['분양가격(㎡)'].fillna('0')
자 다시 형변환을 시켜 보시면.. 또 에러가 나옵니다...... 이번 에러는 invalid literal for int() with base 10: '6,657' 으로 값에 백단위 구분인 콤마(,)가 있어서 숫자형태로 변경이 불가능하다는 것입니다. 이때는 문자열 변경함수인 replace 함수를 이용하여 콤마를 제거 해줘야 합니다.
df['분양가격(㎡)']=df['분양가격(㎡)'].str.replace(',','')
자 다시 형변환시???? 또 에러입니다. invalid literal for int() with base 10: '-' 즉 마이너스 기호가 있다는 뜻으로 이것 역시 replace를 통해 제거 해주시면 됩니다.
df['분양가격(㎡)']=df['분양가격(㎡)'].str.replace('-','')
df['분양가격(㎡)']=df['분양가격(㎡)'].str.replace('','0')
df['분양가격(㎡)']=df['분양가격(㎡)'].astype('Int64')
위와 같이 '-' 마이너스 기호, 그리고 '' NaN도 아닌 아무것도 없는 값을 '0'으로 변경 해주시면 드디어 아래와 같이 Int64로 형변환이 완료됩니다.
- 평균값 대체
자 이젠 다시 돌아가서 분양가격의 결측치에 값을 채워넣기 위해 평균값을 뽑아 볼까요?
null_value=df['분양가격(㎡)'].mean()
print(null_value)
df['분양가격(㎡)']=df['분양가격(㎡)'].fillna(null_value)
null_value 변수에 평균값을 입력하고 fillna 함수를 이용하여 결측값을 모두 평균값으로 변경하였습니다. 다시 결측치 개수를 출력해보면 결측치가 없음을 확인 할 수 있습니다.
- 중앙값 대체
평균값은 모든 값들을 더한 후 개수로 나눈 값으로 이는 데이터의 "이상치(Outliers)"에 따라 값의 영향도가 크게 나타 날 수 있습니다. 정상적인 데이터 분포에서는 평균값을 사용하나 데이터중 이상치가 있다면 중앙값을 사용하여야 합니다. 중앙값은 데이터를 크기별로 정렬한 후 중간에 위치한 값을 말합니다.
사용 방법은 mean()함수 사용과 동일한 구조로 median()함수를 이용하면 됩니다.
null_value=df['분양가격(㎡)'].median()
print(null_value)
df['분양가격(㎡)']=df['분양가격(㎡)'].fillna(null_value)
- 최빈값 대체
다른 방법으로는 최빈값으로 대체 해볼까요? 최빈값을 뜻은 데이터셋 내에서 가장 자주 등장하는 값을 의미합니다. 가장 많은 값,높은 빈도수(frequency)이 겠지요.
최빈값으로 대체하기 위해서는 먼저 값을 세어주는 함수 value_counts()를 활용하여 최빈값을 알아내야 합니다. 이후 value_counts().index[0]을 출력하여 index 가장 첫번째 값을 지정할 수 있습니다.
#분양가격중 가장 많이 사용된 금액의 분포를 확인
null_value=df['분양가격(㎡)'].value_counts()
print(null_value)
#가장 많이 사용된 값의 index값을 지정하여 결측값에 업데이트
null_value=df['분양가격(㎡)'].value_counts.index[0]
print(null_value)
확인 결과 최빈값은 우리가 형변환 시킬때 사용한 0으로 320건 나타납니다. 그러나 0은 임의로 설정한 값이기에 실질적으로 Index[1]의 값이 (20202010) 가장 많이 사용된 최빈값입니다.
2.이상치 처리
이상값 (Outlier)란 데이터 분포 내에서 다른 값들과 동떨어진 값을 의미합니다. 이는 데이터의 평균값을 계산할 때 의도와 다르게 크게 작용할 수 있기 때문에 데이터 활용도에 맞춰 이상값을 확인 후 처리 /유지 하는 방향을 정해야 합니다.
이상값 탐지시에는 IQR (사분위 범위) 기반의 이상값 탐지를 주로 사용합니다. IQR는 데이터의 중간 50% 범위 Q1~Q3를 나타냅니다. 참고로 사분위(Quantile)은 데이터를 작은 값부터 큰 값까지 정렬 후 이를 100%기준으로 정렬후 분활한 값을 의미합니다.
Q1~Q3가 데이터의 중간값 50%인 이유는 Q1(25%)는 데이터의 하위 25%를 포함하는 구간의 끝 지점이며, Q3(75%) 데이터의 하위 75%까지 포함하는 구간의 끝지점으로 Q3-Q1=75%-25%=50%가 되어 Q1~Q3 사이에는 전체 데이터의 50%가 포함되어 있고 이를 중간 50% 범위 (Interquartile Range,IQR)라고 부릅니다.
소스를 한번 볼까요?
Q1=df['분양가격(㎡)'].quantile(0.25)
Q3=df['분양가격(㎡)'].quantile(0.75)
IQR=Q3-Q1
#이상값 기준범위
lower_bound =Q1-1.5*IQR
upper_bound=Q3+1.5*IQR
outliers=df[(df['분양가격(㎡)'] < lower_bound)|(df['분양가격(㎡)'] > upper_bound)]
print("Q1:", Q1)
print("Q3:", Q3)
print("IQR:", IQR)
print("Lower Bound:", lower_bound)
print("Upper Bound:", upper_bound)
print("\n[이상값]")
print(outliers)
Q1은 25% 구간의 끝지점 값, Q3는 75%지점의 끝지점 값으로 IQR=Q3-Q1으로 생성하였습니다.
자.. 여기서 이상값 범위를 계산하기 위해 최저 값은 Q1-1.5*IQR, 최고값은 Q3+1.5*IQR 공식을 대입하였습니다. 좀 더 간격을 넓게 하는 것입니다.
왜 1.5 가 곱해질까요? 1.5는 통계적으로 이상값을 정의하기 위한 기준선으로 정규분포 가정없이 이상값을 탐지하는 경험적 기준치(empirical constant) 입니다. 자세히 말하면 통계학자 Turky의 논문에 소개된 것으로 99.3%의 데이터는 IQR ± 1.5 범위 내에 있을 것으로 가정하는 보수적 추정입니다.
만약 더 보수적으로 민감도를 조절한다면, 2 또는,3 으로 설정이 가능하며, 덜 민감하게 판단될 경우 1을 곱할 수 있습니다. 이렇게 lower_boud 값과, upper_bound 값을 정한 후 해당 값보다 작거나, 큰 범위에 있는 값들을 outliers 변수에 저장을하였습니다. 출력값은 아래와 같습니다. 역시 우리가 초기에 형변환을 위해 대체한 값 0 은 여전히 걸리적 거리는 군요. ㅎㅎ
데이터의 범위를 벗어나는 이상치가 발견되었다면 아래의 3가지 방식으로 이상치 처리가 가능합니다.
- 이상치 제거 (제외처리)
- 이상치 대체 (보정 처리)
- 이상치 유지 (구분/마스킹 처리)
2-1 이상치 제거
이상치를 제거하면 모델에 미치는 왜곡 현상을 원천적으로 손쉽게 제거할 수 있습니다. 다만 이상치 중 유의미한 정보의 데이터에 대한 손실로 인사이트를 놓칠수 있는 위험은 항상 존재하기에, 사용데이터의 방향에 맞게 이상치를 검증 후 결정을 하여야 합니다.
#DF 데이터 셋을 df_clean 데이터 셋으로 복사
df_clean=df.copy()
#df_clean 데이터에서 이상치 범위의 데이터 제거
df_clean = df_clean[(df_clean['분양가격(㎡)'] >= lower_bound) & (df_clean['분양가격(㎡)'] <= upper_bound)]
print (df.info())
print (df_clean.info())
우선 df 데이터셋을 df_clean 데이터셋에 복사를 합니다. 이후 db_clean 데이터 중 lower_bound와 upper_bound사이의 값들만 선택하여 이상치를 제거 하면 됩니다. 아래와 같이 기존 4505행의 데이터 중 이상치를 제거한 3872개의 데이터 행이 생성 됩니다.
2-2 이상치 대체
대표적으로 평균값, 중간값, 그리고 예측값으로 이상치를 대체 할 수 있으며 전체 데이터 손실없이 안정적으로 데이터를 유지 할 수 있는 장점이 있는 반면, 평균을 과도하게 사용할 경우 통계적 왜곡의 가능성이 항상 존재합니다.
결측치 대체에서 사용했던 방식과 동일하게 평균값은 mean() 함수, 중간값은 median()함수를 사용하여 값을 대체 할 수 있으며, 다른 변수들의 관계를 이용하여 합리적인 예측값으로도 보정할 수도 있습니다.
- 예측값
예측값은 회귀기반 대체 방법(Model-Based Imputation)이라고도 하며 통계/기계학습적으로 가장 정교하고 유연한 이상치 보정 기법입니다. 예측값을 사용하기 위해서는 아래와 같이 명확한 시나리오를 사전에 준비하여야 합니다 .
- DataFrame : 지역, 규모, 연/월 별 분양가격 정보
- 이상치 컬럼 : 분양가격 분양가격(㎡)
- 목표 : 다른 컬럼 (지역명, 규모 구분)을 바탕으로 분양가격(㎡)을 예측, 이상치 대체
회귀모델을 사용하기 위해서는 파이썬의 대표 머신러닝 라이브러리 인 "scikit-learn" 이 설치되어 있어야 합니다.
pip install scikit-learn
import pandas as pd
import numpy as np
import openpyxl
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import LabelEncoder
#머신러닝을 위해서는 텍스트를 수치화 해야 합니다. 범주화 예제
label_cols = ['지역명', '규모구분'] # 여기는 반드시 컬럼명이어야 합니다
for col in label_cols:
le = LabelEncoder()
df[col + '_code'] = le.fit_transform(df[col].astype(str))
print(df.info())
# 이상치 마스킹, upper_bound는 사전에 계산된 이상치 기준값 (Q3_1.5 *IQR)으로초과 값애 대해서 이상치 판단.
df['IsOutlier']=df['분양가격(㎡)'] > upper_bound
#이상치가 아닌 데이터 만 학습에 사용
train=df[~df['IsOutlier']]
x_train=train[['지역명_code','규모구분_code']]
y_train=train['분양가격(㎡)']
#이상치 데이터는 예측값으로 사용
test = df[df['IsOutlier']]
X_test = test[['지역명_code', '규모구분_code']]
#학습 및 예측용 입력 변수에 결측값이 있다면 모델 학습이 되지 않기에 결측값 제거
x_train = x_train.dropna()
X_test = X_test.dropna()
#회귀 모델 훈련
#x_train의 입력값 (지역,규모)을 기반으로 분양가격 (y_train)을 예측하는 선형 회귀 모델 학습
model = LinearRegression()
model.fit(x_train,y_train)
# 이상치 예측후 대체
#x_test 에 대해 예측된 가격을 계산하고, 기존 이상치위치에새로운예측값으로 대체
#df.loc[df['IsOutlier'], '분양가격(㎡)'] = model.predict(X_test)
df.loc[X_test.index, '분양가격(㎡)'] = model.predict(X_test).astype(int)
# 결과 확인
print(df[['지역명_code', '규모구분_code', '분양가격(㎡)']])
우선 머신러닝을 위해서는 텍스트 데이터를 수치화 해야합니다(범주형 변수 인코딩). 이를 위해 LabelEncoder() 함수를 이용하여 지역명, 규모구분 두 컬럼을 러신머닝에 사용할 수 있도록 각 고유를 텍스트를 정수로 반환 (서울=0, 부산=1, 대전=3...) 후, '지역명_code' ,'규모구분_code'열을 추가로 생성하였습니다.
그 후 학습용 데이터와 예측 대상 데이터를 분리하여, 학습용 데이터는 선형 회귀모델 (LinearRegression())로 학습 후 이상치를 예측 값으로 대체 하는 코드입니다. 평균, 중간값보다 검증된 데이터를 활용하는 방법이랍니다.
여기까지 결측치와 이상치처리를 진행하며 우리는 3단계 데이터 타입변환, 4단계 범주형 변수 인코딩을 함께 진행하였습니다. 기본적이지만 전처리에 있어서 대부분을 차지하는 단계까지 현재 진행되었답니다.
나머지 단계는 (5단계 스케일링, 6단계 파생 변수 생성, 7단계 불필요한 컬럼제거) 데이터의 활용도에 맞게 선택적으로 진행하시면 된답니다.
이젠 데이터 분석,예측에 직접 활용해 보시기 바랍니다 화이팅~.
'IT > Python 데이터 분석 활용' 카테고리의 다른 글
[의사결정나무] 출근시간이 성과에 미치는 영향 분석(Decision Tree) (2) | 2025.06.18 |
---|---|
[회귀분석] 근무시간이 업무성과에 미치는 영향 분석 (1) | 2025.06.18 |
[Python] 시각화 plot() (Matplotlib,Pandas) (1) | 2024.06.04 |
[Python] Pandas[판다스] 활용하기(2) (0) | 2024.05.30 |
[Python] Pandas[판다스] 활용하기(1) (0) | 2024.05.29 |
댓글