TIL - today I learned

[멋쟁이사자처럼 데이터분석 부트캠프] - EDA # 데이터 정제

heehee_ 2025. 2. 16. 04:52

데이터의 정제

데이터로부터 원하는 결과나 분석을 얻기 위해서 수집된 데이터를 분석의 도구 또는 기법에 맞게 다듬는 과정이 필요

데이터 정제 방법

  • 제거
    • 결측치 제거: `df.dropna()`
    • 컬럼/행 제거
    • 이상치 제거
  • 집계
    • 데이터의 요약 / 통계 데이터 확인
    • 데이터 특성 파악
  • 일반화
    • 데이터의 일반적인 특성이나 패턴을 추출
    • 일관된 분석 및 예측이 가능하도록
  • 정규화
    • 일정한 범위로 조정해서 데이터 간의 상대적인 크기 차이를 제거하여 데이터를 표준화하는 작업
    • 데이터의 상대적인 비교를 용이하게 하고, 이상치에 대한 영향을 완화시킨다
  • 평활화
    • 데이터의 변동을 줄이고 노이즈를 제거한다.
    • 데이터의 추세나 패턴을 부드럽게 만든다

결측치 제거

결측치 확인

- df.info() 로 전체 행의 수와 non-null값을 확인해서 결측치 개수 확인

- df.isna().sum()으로 각 인덱스마다 몇 개의 결측치가 있는지 한 눈에 확인

 

결측치 처리 

 

1) 임의 제거 (`dropna`)

df.dropna(subset=['컬럼명'], how=' ', inplace=True)
  • df.dropna(how='any'): NaN이 하나라도 있으면 삭제
  • df.dropna(how='all'): 모두  NaN인 행만 삭제

❗`df.drop(columns='컬럼명')` 에서는 columns를 사용 → 컬럼을 제거하는 것이기 때문에

❗`df.dropna(subset='컬럼명')` 에서는 subset 사용 → 특정 컬럼 안의 결측치를 제거하는 것이기 때문에

# payment컬럼에서 결측치 제거
df=df['payment'].dropna()		#ver1
df=df.dropna(subset=['payment'])		#ver2

# 두가지 이상의 컬럼에서 모두 결측치가 있는 행만 제거
df.dropna(subset=['pickup_borough', 'dropoff_borough'], how='all', inplace=True)

 

 


2) 임의 대체  (`fillna`)

 

   결측치 대체 방법

  • 평균 대치법: 데이터의 평균으로 결측치 대체
  • 회귀 대치법: 결측치를 다른 데이터와 회귀분석을 통해  대체
  • 단순확률 대치법: 확률추출에 의해 전체 데이터 중 무작위로 추출
  • 최근접 대치법: 응답자료를 순서대로 정리한 후, 결측치 바로 이전/이후의 응답으로 대체(ffill,bfill)
# dropoff_borough 대해서 pickup_borough 값으로 대체
# 이전값/이후값으로 대체
# 최빈값으로 대체
df['dropoff_borough'].fillna(df['pickup_borough'], inplace=True)
df['dropoff_borough'].fillna(method='ffill',inplace=True)
df['dropoff_borough'].fillna(method='bfill',inplace=True)
df['dropoff_borough'].fillna(df['dropoff_borough'].mode()[0], inplace=True)

이상치 처리

  • IQR(Interquartile Range)
    • 중간 50% 데이터의 범위 (Q3-Q1)
    • 이상치: IQR을 기준으로 1.5배 IQR범위를 벗어난 데이터
  • Z-score
    • 표준화 스코어; 평균에서 얼마나 떨어져 있는지를 지표화
    • 이상치: 절대값이 3보다 큰 데이터

데이터가 정규분포를 따른다면 z-score 방법으로, 정규분포가 아닐경우(왜도가 있거나, 비대칭적이라면) IQR방법으로


IQR(사분위범위)

데이터의 중앙 50%를 나타내는 지표

Q1(1사분위)와 Q3(3사분위) 사이의 범위

https://towardsdatascience.com/understanding-boxplots-5e2df7bcbd51

  • Q1: 데이터의 하위 25%에 해당하는 값  `Q1 = df['컬럼명'].quantile(0.25)`
  • Q3: 데이터의 하위 75%에 해당하는 값  `Q3 = df['컬럼명'].quantile(0.75)`
  • IQR = Q3 - Q1
#이상치 경계값 설정
minimum(lower_bound) = Q1 - 1.5 * IQR
maximum(upper_bound) = Q3 + 1.5 * IQR

#이상치 제거
df_no_outlier = df[(df['컬럼명'] >= lower_bound) & (df['컬럼명'] <= upper_bound)]

Z-score

데이터가 평균으로부터 얼마나 떨어져 있는지를 표준편차 단위로 나타내는 값.

➡️특정 값이 전체 데이터에서 얼마나 극단적인지를 판단하는데 사용됨

# z-score 기반 이상치 제거 모듈 설치
from scipy import stats
import numpy as np

# Z-Score 계산
z_scores=stats.zscore(df['컬럼명'])

# 이상치 경계값 설정
df_no_outliers=df[(z_scores < 3) & (z_scores > -3)] #ver1
df_no_outliers=df[np.abs(z_scores)<3]		#ver2

# 이상치 개수 확인
outlier_count = (np.abs(z_score) > 3).sum()

 

`np.abs(z_score) > 3` 는True(1) / False(0)의 형태로 값이 출력

>> 개수를 알고 싶다면 sum()을 사용

 


이상치 대체(평균)

# 이상치 대체 (평균)
import numpy as np

# 이상치가 아닌 값들의 평균 계산 (IQR사용)
mean_value = df[(df['컬럼명'] >= lower_bound) & (df['컬럼명'] <= upper_bound)['컬럼명'].mean()

# 이상치를 평균값으로 대체
df['컬럼명'] = np.where((df['컬럼명'] < lower_bound) | (df['컬럼명'] > upper_bound), mean_value, df['컬럼명'])

 

 

mean_value 

  1. 이상치가 아닌 것들을 필터링 `df[(df['컬럼명'] >= lower_bound) & (df['컬럼명'] <= upper_bound)`
  2. 필터링된 데이터 프레임중에서 특정 컬럼만 선택해서 평균을 계산 `df_filtered['컬럼명'].mean()`

np.where(조건, True, False)

조건을 만족하는 요소의 위치를 반환해서 지정한 값으로 반환

  1. 이상치를 평균값으로 대체할 것이기 때문에, 이상치를 조건으로 설정 ( | : or)
  2. 이상치이면 mean_value(이상치가 아닌 값들의 평균), 이상치가 아니면 기본 데이터값으로 반환

 

이상치 대체(이전값, 이후값) - 시계열 데이터

#이전값 대체
df['value'] = df['value'].where((df['value'] <= lower_bound) | (df['value'] >= upper_bound)).fillna(method='ffill') #ver2

 

np.where대신 df['컬럼명'].where을 쓰는 경우

np.where()도 사용가능하지만, df.where()은 기존 데이터 프레임의 구조를 유지하면서 조건을 만족하는 값에만 적용하기 때문에 시계역 데이터를 다룰 때 더욱 안정성있게 사용가능함.


범주화

# 구간을 나누어 범주화후 라벨링
ages = [15, 23, 35, 42, 19, 28, 33, 51, 62, 18, 20, 41]
bins=[0,20,40,60,80]
labels=['청소년', '청년', '중년', '장년']

age_groups = pd.cut(ages,bins=bins, labels=labels)
age_groups

#결과
['청소년', '청년', '청년', '중년', '청소년', ..., '중년', '장년', '청소년', '청소년', '중년']

 

  • bins: 범주의 끝과 시작을 포함 ▶ 나누고자 하는 범주의 개수보다 +1
  • labels = 각 구간의 이름

각 구간을 인덱스로 변환

agegroup_idx = np.digitalize(ages,bins)
agegroup_idx

#결과
array([1, 2, 2, 3, 1, 2, 2, 3, 4, 1, 2, 3])

정규화

데이터의 범위가 심하게 차이나는 경우, 상대적 특성이 반영된 데이터로 변환하는 과정

🌟컬럼마다 단위가 다른 경우, 정규화를 고려해볼 필요가 있음

 

출처:꼬꼬마코더 tistory

 

 

정규화의 종류

  • 일반 정규화: 수치로 된 값에 대해서 동일한 수치로 변환
  • Min-Max정규화: 최소값은 0, 최대값은 1, 그 외 값은 0과 1사이의 값으로 변환
  • Z-score 정규화: 평균은 0, 편차가 1인 정규분포의 데이터로 변환
  • 로그 변환: 데이터분포의 형태에서 꼬리가 오른쪽으로 기울어진 경우에, 데이터를 로그데이터로 변환시키며 정규화
  • 역수변환: 데이터분포의 형태에서 꼬리가 극단적으로 오른쪽으로 기울어진 경우 에, 데이터를 역수로 변환시키며 정규화
  • 제곱근(루트) 변환: 데이터 분포의 형태에서 꼬리가 오른쪽으로 치우친 경우에, 데이터를 제곱근 데이터로 변환하며 정규화
  • 지수변환: 데이터분포의 형태에서 꼬리가 왼쪽으로 기울어진 경우 에, 데이터를 지수로 변환시키며 정규화

 

1. Min-Max Scaling(최소-최대 정규화) - MinMaxScaler

  • X: 원래 데이터 값
  • Xmin: 해당 컬럼의 최솟값
  • Xmax: 해당 컬럼의 최댓값
  • X1: 정규화된 값(0~1 범위)

✔️ 데이터가 고르게 분포되어있을 때 적합.

✔️ 최댓값과 최솟값에 영향을 받기 때문에 이상치에 매우 민감 

# Min-Max 정규화 ver1
df['Min_Max_Scaled'] = (df['Value'] - df['Value'].min()) / (df['Value'].max() - df['Value'].min())

# Min-Max 정규화 ver2
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
df['Normalized'] = scaler.fit_transform(df[['Value']])

 

2. Z-Score Normalization(표준화) - StandardScaler

  • μ: 평균값
  • : 표준편차

✔️ 데이터가 정규분포를 따를 때 효과적

      ❓데이터가 정규분포를 따르는데 왜 표준화를 해야할까?

      → 컬럼마다 각각 다른 단위를 가진 정규분포일수도

✔️ 극단적이 값이 있어도, 데이터의 상대 위치를 유지함

✔️ 거리 기반 모델에서 변수 크기의 차이를 보정하는데에 많이 사용됨

# z-score 정규화 ver1
df['Z_Score_Scaled'] = stats.zscore(df['Value'])

# z-score 정규화 ver2
from sklearn.preprocessing import StandardScaler
# Z-Score 표준화 적용
scaler = StandardScaler()
df['Standardized'] = scaler.fit_transform(df[['price']])
#여러 컬럼에 z-score정규화를 적용하는 경우- ver1
df_zscore = df.apply(stats.zscore)

#결과
          A         B
0 -1.414214 -1.414214
1 -0.707107 -0.707107
2  0.000000  0.000000
3  0.707107  0.707107
4  1.414214  1.414214


#여러 컬럼에 z-score정규화를 적용하는 경우- ver2
df_zscore = stats.zscore(df)

#결과
[[-1.414214  -1.414214]
 [-0.707107  -0.707107]
 [ 0.000000   0.000000]
 [ 0.707107   0.707107]
 [ 1.414214   1.414214]]
  • `df.apply(stats.zscore)` : 각 컬럼을 개별적으로 Pandas DataFrame으로 변환 ✅권장
  • `stats.zscore(df)` : 전체 DateFrame을 Numpy배열로 변환 → 후처리가 필요할 수도 있음.

 

3. Robust Scaling

중앙값과 IQR을 사용해서 이상치에 영향을 덜 받도록 하는 정규화

✔️ 이상치에 영향을 덜 받음

✔️ 데이터가 정규분포가 아닐 때에도 효과적

➡️ 이상치가 많은 금융데이터나, 지진강도와 같은 데이터에 적용하기에 적합

 

 

4. 로그 변환(Log Transformation)

✔️ 값이 큰 경우에 효과적

✔️ 음수 값이나 0이 포함되면 적용할 수 없음.

df['Log_price'] = np.log(df['price'])

 

5. 역수 변환(Reciprocal Transformation)

 

데이터 값을 역수로 변환해서 분포를 조정하며 정규화

✔️ 오른쪽으로 기울어짐을 완화

✔️ 값이 클수록 변환 후에는 작은 값이 됨

✔️ 0이 포함되면 적용 불가

✔️ 데이터가 비선형인 경우에 활용

df['Inverse_Value'] = 1 / df['Value']

 

5. 제곱근 변환(Square Root Transformation)

✔️ 오른쪽 치우친 데이터 정규화

✔️ 값이 클수록 더 큰 영향을 받는다.

✔️ 로그 변환보다 완만하게 변형한다.

✔️ 음수 적용 x

df['SquareRoot_Value'] = np.sqrt(df['Value'])

 

6. 지수 변환(Exponential Transformation)

 

데이터에 지수(exp)를 적용해서 분포를 조정하는 방법

로그 변환과는 반대 역할

✔️왼쪽 치우침 완화

✔️ 데이터의 값이 작을수록 더 빠르게 증가

✔️ 값이 커질수록 데이터가 급격히 증가해서 이상치가 강조될 수도 있음.

df['Exp_Value'] = np.exp(df['Value'])
df['Squared_Value'] = np.power(df['Value'], 2)
df['Squared_Value'] = np.power(df['Value'], 3)

 


인코딩

문자형 데이터를 숫자로 변환하는 과정

머신러닝 모델은 숫자데이터만을 처리하기 때문에, 범주형 데이터를 수치형 데이터로 변환하는 과정을 거쳐야함.

 

  • 레이블 인코딩: 각 카테고리 를 정수로 변환
  • 원-핫 인코딩: 각 카테고리를 0과 1로 변환
  • 타깃 인코딩: 각 카테고리에 대한 종속변수를 평균값으로 변환, 분류문제에 주로 활용되며, 과적합 가능성이 있음

✔️ 순서(크기)가 없는 범주형 데이터의 인코딩 방식: 원-핫 인코딩

✔️ 순서(크기)가 있는 범주형 데이터의 인코딩 방식: 레이블 인코딩, 순서 인코딩

 

1. 레이블 인코딩(Label Encoding)

각 카테고리에 고유한 정수를 할당하는 방식

 

✔️ 카테고리형 데이터를 정수로 변환하는 경우에 사용

✔️ 하지만, 순서 관계가 생길 위험이 있다.

      예를 들어 모델은 apply < banana < orange와 같은 서열로 인식할 수 있음.

     ➡️ 원-핫 인코딩 사용

#범주를 지정된 라벨로 변환 -ver1
category_mapping={'Low' : 0, 'Medium':1, 'High':2}
df['Category_Encoded']=df['Category'].map(category_mapping)

#라벨 인코딩-ver2
from sklearn.preprocessing import LabelEncoder
fruits = ['Apple', 'Banana', 'Orange', 'Banana', 'Apple']
encoder = LabelEncoder()
encoded_labels = encoder.fit_transform(fruits)
print(encoded_labels) 

#결과
[0 1 2 1 0]

 

 

2. 원-핫 인코딩(One-Hot Encoding) 

각 카테고리를 0과 1의 벡터로 변환하는 방식

 

✔️ 각 카테고리가 개별적인 컬럼이 되고, 값이 있으면 1, 없으면 0으로 출력된다. 

✔️ 순서 정보가 없어 각 카테고리가 독립적으로 표현된다.

✔️ 카테고리 개수가 많아지면 차원이 증가해 문제가 발생함.

 

Ver1. - get_dummies활용

#One-Hot encoding ver1

import pandas as pd

# 예제 데이터
df = pd.DataFrame({'Fruit': ['Apple', 'Banana', 'Orange', 'Banana', 'Apple']})

# One-Hot Encoding 적용
df_one_hot = pd.get_dummies(df, columns=['Fruit'])
print(df_one_hot)

   Fruit_Apple  Fruit_Banana  Fruit_Orange
0            1             0             0
1            0             1             0
2            0             0             1
3            0             1             0
4            1             0             0

 

Ver2. - OneHotEncoder()사용

#One-Hot encoding ver2

# OneHotEncoder 객체 생성
encoder = OneHotEncoder(sparse=False)  # sparse=False로 하면 배열 형태로 결과를 반환

# 데이터를 fit_transform()을 통해 변환
encoded_data = encoder.fit_transform(df[['Category']]).toarray()

# 결과를 DataFrame으로 변환
encoded_df = pd.DataFrame(encoded_data, columns=encoder.get_feature_names_out(['Category']))

# 원본 DataFrame과 인코딩된 DataFrame을 합침
df = pd.concat([df, encoded_df], axis=1)

print(encoded_df)


#결과
    Fruit  Fruit_Apple  Fruit_Banana  Fruit_Orange
0   Apple          1.0           0.0           0.0
1  Banana          0.0           1.0           0.0
2  Orange          0.0           0.0           1.0
3  Banana          0.0           1.0           0.0
4   Apple          1.0           0.0           0.0

 

❗pandas의 get_dummies()는 직관적이고 빠르게 One-Hot Encoding 적용 가능

❗sklearn의 OneHotEncoder()는 머신러닝 파이프라인에서 사용하기 적합하다.

     Numpy배열로 출력되기 때문에 후처리가 필요하다는 번거로움이 있음

 

3. 순서 인코딩(Ordinal Encoding)

서열(순서)이 있는 범주형 데이터를 숫자로 변환하는 방식

 

✔️ 잘못 사용하면 모델이 순서정보를 잘못해석할 수 있기 때문에 순서 관계를 유지해야하는 경우에만 적합

from sklearn.preprocessing import OrdinalEncoder

#예제 데이터
df = pd.DataFrame({'Size': ['Small', 'Medium', 'Large', 'Medium', 'Small']})

#순서형 인코딩 적용
encoder=OrdinalEncoder(categories=[['Small', 'Medium', 'Large']]) #순서지정
df['Size_Encoded']=encoder.fit_transform(df[['Size']])

 

 

4. 타켓인코딩(Target Encoding)

분류모델에서 카테고리별 타겟값의 평균을 사용해서 인코딩

 

예제 데이터

 

 

카테고리별 평균 구매율 계산

  • A: (1+0+1)/3=0.67(1+0+1)/3 = 0.67
  • B: (1+1)/2=1.00(1+1)/2 = 1.00
  • C: (0)/1=0.00(0)/1 = 0.00

 

인코딩 결과

 

 

import pandas as pd

# 원본 데이터
df = pd.DataFrame({
    'Category': ['A', 'A', 'A', 'B', 'B', 'C'],
    'Purchase': [1, 0, 1, 1, 1, 0]  # 타겟 변수 (구매 여부)
})

# 타겟 인코딩 적용 (카테고리별 평균 계산)
df['Category_Encoded'] = df.groupby('Category')['Purchase'].transform('mean')
728x90