데이터 클리닝 및 전처리 기법
import pandas as pd
df = pd.read_excel('car.xlsx')
df.head() # 데이터 첫 5줄 보기
mileage model price year my_car_damage other_car_damage
0 63608 K3 970 2017 0 564596
1 69336 K3 1130 2015 1839700 1140150
2 36000 K3 1380 2016 446520 2244910
3 19029 K3 1390 2017 889000 4196110
4 97090 K3 760 2015 2339137 2029570
결측값 처리
import numpy as np
np.random.seed(42) # 재현을 위해 씨앗값을 42로
miss_prob = 0.1 # 10%의 데이터를 결측치로
df_miss = df.copy() # df를 복사
df_miss['mileage'] = df_miss['mileage'].apply(
lambda x: np.nan if np.random.rand() < miss_prob else x
# 10%의 확률로 x를 nan(Not A Number, 결측값)으로 대치
)
원래 데이터 크기
df.shape
(274, 6)
결측치 있는 행 보기
df_miss[df_miss['mileage'].isnull()].head()
# df_miss['mileage'].isnull(): mileage의 결측 되었는가?
# df_miss[ ... ]: 위의 조건이 참인 행만
mileage model price year my_car_damage other_car_damage
6 NaN K3 899 2015 2229758 1877410
10 NaN K3 1490 2015 0 0
29 NaN K3 890 2012 1680780 147980
32 NaN K3 780 2013 603000 0
37 NaN K3 800 2014 3004070 4657280
mileage에 결측치 없는 행 보기
df_miss[df_miss['mileage'].notnull()].head()
# df_miss['mileage'].isnull(): mileage의 결측 안되었는가?
# df_miss[ ... ]: 위의 조건이 참인 행만
mileage model price year my_car_damage other_car_damage
0 63608.0 K3 970 2017 0 564596
1 69336.0 K3 1130 2015 1839700 1140150
2 36000.0 K3 1380 2016 446520 2244910
3 19029.0 K3 1390 2017 889000 4196110
4 97090.0 K3 760 2015 2339137 2029570
df_miss.dropna().head()
mileage model price year my_car_damage other_car_damage
0 63608.0 K3 970 2017 0 564596
1 69336.0 K3 1130 2015 1839700 1140150
2 36000.0 K3 1380 2016 446520 2244910
3 19029.0 K3 1390 2017 889000 4196110
4 97090.0 K3 760 2015 2339137 2029570
df['mileage'].mean()
77483.22262773722
결측치가 있는 데이터의 평균(원래와 달라지지만 오차가 커진 것일 뿐 더 편향된 것은 아님)
df_miss['mileage'].mean()
79713.21224489796
기본적으로 결측치는 삭제하고 평균냄(skipna=True). 필요는 없지만 굳이 쓴다고하면 결측치도 확인하고 싶고, 결측치가 없을 때는 평균도 내고 싶을 때 한 줄로 처리하려는 의도.
df_miss['mileage'].mean(skipna=False)
nan
아는 값과 모르는 값(결측치)를 계산하면 그 결과도 모름. 하나라도 결측치가 포함이 되어있으면 통계치를 계산할 수 없음
1 + np.nan
nan
KDE(Kernel Density Estimation): 확률분포를 대략 추정하는 기법
정규분포는 언제 생기나? 독립적인 변수들이 많이 있어서 서로 더해질 때 생김. 키에 영향을 주는 유전자 10000개 쯤 있음(사람마다 태어날때 동전 10000개를 던져서 그 합계만큼이 자기 키)
두 개의 플롯 그리는 코드를 한 셀에서 실행하면 겹쳐서 그려줌
import seaborn as sns
import matplotlib.pyplot as plt
sns.kdeplot(df['mileage'], label='original')
sns.kdeplot(df_miss['mileage'], label='missing')
plt.legend() # 범례 표시
<matplotlib.legend.Legend at 0x17ff87e88e0>
<Figure size 640x480 with 1 Axes>
평균으로 대체하면 결측값을 모두 평균으로 바꾸기 때문에 변동성이 감소
m = df_miss['mileage'].mean() # 평균
impute_mean = df_miss['mileage'].fillna(m) # 결측값을 대체
sns.kdeplot(df['mileage'], label='original')
sns.kdeplot(impute_mean, label='mean')
plt.legend()
<matplotlib.legend.Legend at 0x17fea46dd30>
<Figure size 640x480 with 1 Axes>
KNN(K Nearest Neighbor): K개의 가장 비슷한 사례를 참조
비슷하다는 것은 결측되지 않은 값이 비슷하다는 것(거리를 계산해서 거리가 가까움) 루트((x1 - x2)**2 + (y1 - y2)**2)
K=2: 비슷한 사례를 2개 찾은 다음 평균을 냄
from sklearn.impute import KNNImputer
imputer = KNNImputer(n_neighbors=2, weights="uniform")
X = imputer.fit_transform(df_miss[['mileage', 'price', 'year']])
impute_knn = X[:, 0]
sns.kdeplot(df_miss['mileage'], label='original')
sns.kdeplot(impute_knn, label='knn')
plt.legend()
<matplotlib.legend.Legend at 0x17fea649610>
<Figure size 640x480 with 1 Axes>
삭제나 대체로는 평균에는 큰 차이가 없을 수도 있다.
df['mileage'].mean()
77483.22262773722
df_miss['mileage'].mean() # 리스트 삭제를 했을 때 평균
79713.21224489796
impute_mean.mean() # 평균 대체를 했을 때 평균
79713.21224489798
impute_knn.mean() # 예측 대체를 했을 때 평균
78922.5602189781
표준편차: 데이터가 퍼져 있는 정도
df['mileage'].std()
43015.79445317075
df_miss['mileage'].std()
44020.89207331192
impute_mean.std() # 평균 대체를 하면 표준편차가 감소(똑같은 값으로 채워넣기 때문에)
41617.15891448403
impute_knn.std() # 예측 대체를 했을 때 표준편차가 조금 덜 감소(차마다 채워넣는 값이 달라서)
42832.918597044976
표준편차가 원본 데이터랑 가깝게 나오는게 대체가 적당히 잘 되었다는 것을 수치적으로 볼 수 있는 건가요? 원본데이터보다 너무 차이가 나지 않는 대체를 하는 것이 좋다는 뜻으로 이해해도 되나요?
표준편차만이 아니라 원본 데이터의 분포에 가깝게 나와야함(모든 통계치가 똑같게 나와야한다는 뜻)
표준편차도 다르다면 다른 통계치도 다르게 나올 수 있음
현실에서는 이걸 확인해볼 수가 없음(우리는 결측된 데이터만 볼 수 있고, 원본은 볼 수 없음)
상당 부분이 가정에 의존함
df.mileage.min() # 최솟값
2287
df.mileage.max() # 최댓값
310000
df.mileage.mean() # 평균
77483.22262773722
df.mileage.std() # 표준편차
43015.79445317075
스케일링
정규화
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
mileage_normalized = scaler.fit_transform(df[['mileage']])
mileage_normalized[:5]
array([[0.19927985],
[0.2178946 ],
[0.10955988],
[0.05440784],
[0.30808903]])
표준화
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
mileage_standardized = scaler.fit_transform(df[['mileage']])
mileage_standardized[:5]
array([[-0.32315135],
[-0.1897473 ],
[-0.96613652],
[-1.36138794],
[ 0.45663819]])
비대칭 데이터 변환
log_transformed = np.log(df['mileage'])
sns.kdeplot(log_transformed, label='log')
<Axes: xlabel='mileage', ylabel='Density'>
<Figure size 640x480 with 1 Axes>
from scipy import stats
boxcox_transformed, lambda_value = stats.boxcox(df['mileage'])
sns.kdeplot(boxcox_transformed, label='boxcox')
<Axes: ylabel='Density'>
<Figure size 640x480 with 1 Axes>
lambda_value
0.5323138494091241
범주형 데이터
원핫인코딩
from sklearn.preprocessing import OneHotEncoder
encoder = OneHotEncoder(sparse_output=False)
X = encoder.fit_transform(df[['model']])
X[:5]
array([[0., 1.],
[0., 1.],
[0., 1.],
[0., 1.],
[0., 1.]])
df['model'].str.get_dummies()
Avante K3
0 0 1
1 0 1
2 0 1
3 0 1
4 0 1
.. ... ..
269 1 0
270 1 0
271 1 0
272 1 0
273 1 0
[274 rows x 2 columns]
더미 코딩
다중공선성: 여러 개의 변수로 함께 선형적인 성질을 띈다(다른 변수로 조합해서 만들 수 있다)
x1 x2 y 1 1 2 2 2 4
y = x1 + x2, y = 2x1, y = 2x2, y = 3x1 - x2, ..
encoder = OneHotEncoder(sparse_output=False, drop='first')
X = encoder.fit_transform(df[['model']])
X[:5]
array([[1.],
[1.],
[1.],
[1.],
[1.]])
타겟 인코딩
df.groupby('model').agg({'price': 'mean'}) # 모델별 평균가격
price
model
Avante 833.414634
K3 913.811594
from sklearn.preprocessing import TargetEncoder
encoder = TargetEncoder(target_type='continuous')
model_encoded = encoder.fit_transform(df[['model']], df['price'])
model_encoded[:5]
array([[889.81206363],
[907.43802815],
[889.81206363],
[917.55272079],
[915.5145453 ]])