[ML] 3강 - 머신러닝을 위한 전처리 실습

필요한 라이브러리 임포트

import numpy as np
import pandas as pd

# 스케일링, 폴리노미얼, 라벨인코딩, SMOTE
from sklearn.preprocessing import StandardScaler, MinMaxScaler
from sklearn.preprocessing import PolynomialFeatures, LabelEncoder
from imblearn.over_sampling import SMOTE

# VIF 계산용 (다중공선성 확인용)
from statsmodels.stats.outliers_influence import variance_inflation_factor

 

실습 데이터 생성

프로젝트에서는 0번 절차는 생략됨.

 

0-1. 임의 데이터프레임 생성

  • 수치형, 범주형, 날짜, 타깃 포함
# ----------------------------------------------------------
# 1) 임의 데이터프레임 생성
#    - 수치형 변수 2개, 범주형 변수 1개, 날짜변수 1개, 타깃 변수(불균형) 1개
# ----------------------------------------------------------

np.random.seed(42)
N = 200

# 수치형 변수를 생성 (평균 50, 표준편차 10)
num1 = np.random.normal(loc=50, scale=10, size=N)
num2 = np.random.normal(loc=100, scale=20, size=N)

# 범주형 변수 (A, B, C 중 무작위)
cat_col = np.random.choice(['A', 'B', 'C'], size=N)

# 날짜변수(최근 200일 내 임의 날짜) 생성
date_rng = pd.date_range('2025-01-01', periods=N, freq='D')
print(date_rng)
#np.random.shuffle(date_rng)  # 날짜 순서 랜덤화

# 불균형 타깃(1이 10%, 0이 90%라고 가정)
#   - 실제로 1이 대략 20개, 0이 180개
target = np.random.choice([0,1], p=[0.9, 0.1], size=N)

# 데이터프레임 구성
df = pd.DataFrame({
    'num1': num1,
    'num2': num2,
    'cat_col': cat_col,
    'date_col': date_rng,
    'target': target
})

 

0-2. 결측치 추가 & 이상치 주입

# ----------------------------------------------------------
# 2) 결측치 추가 & 이상치(Outlier) 주입
#    - 일부 값을 NaN으로 바꾸고, 극단값 몇 개 삽입
# ----------------------------------------------------------

# (2-1) 결측치 추가
missing_indices_num1 = np.random.choice(df.index, size=5, replace=False)  # 5개 결측
missing_indices_num2 = np.random.choice(df.index, size=5, replace=False)  # 5개 결측
missing_indices_cat = np.random.choice(df.index, size=3, replace=False)   # 3개 결측
missing_indices_date= np.random.choice(df.index, size=2, replace=False)   # 2개 결측

df.loc[missing_indices_num1, 'num1'] = np.nan
df.loc[missing_indices_num2, 'num2'] = np.nan
df.loc[missing_indices_cat, 'cat_col'] = np.nan
df.loc[missing_indices_date,'date_col'] = pd.NaT

# (2-2) 이상치(Outlier) 주입: num1, num2에 극단값
outlier_indices_num1 = np.random.choice(df.index, size=3, replace=False)
outlier_indices_num2 = np.random.choice(df.index, size=3, replace=False)
df.loc[outlier_indices_num1, 'num1'] = df['num1'].mean() + 8*df['num1'].std()
df.loc[outlier_indices_num2, 'num2'] = df['num2'].mean() + 10*df['num2'].std()

print("=== [원본 데이터 일부] ===")
print(df.head(10))
print()
# 결측치 있음을 확인
df.isnull().sum()

1. 결측치 처리

# ----------------------------------------------------------
# 3) 결측치 처리
#    (3-1) 일부 행 제거, (3-2) 평균·중앙값 등으로 대체
# ----------------------------------------------------------

df_dropna = df.dropna()  # 모든 컬럼 중 결측값이 있으면 제거

df_fillna = df.copy()
# 수치형은 열별 평균으로 대체 (mean)
df_fillna['num1'] = df_fillna['num1'].fillna(df_fillna['num1'].mean())
df_fillna['num2'] = df_fillna['num2'].fillna(df_fillna['num2'].mean())
# 범주형은 최빈값으로 대체 (mode)
most_freq_cat = df_fillna['cat_col'].mode().iloc[0]
df_fillna['cat_col'] = df_fillna['cat_col'].fillna(most_freq_cat)
# 날짜열은 제거하지 않고 그대로 둠(또는 임의 날짜로 대체 가능)
# -> 시연을 위해 NaT(결측)도 남겨둠

print("=== [결측치 제거 후 shape] ===")
print(df_dropna.shape)
print("=== [결측치 대체 후 shape] ===")
print(df_fillna.shape)
print()

 

2. 이상치 제거

# ----------------------------------------------------------
# 4) 이상치 제거
#    - (4-1) 표준편차 기준 (mu ± 3*std)
#    - (4-2) IQR 기준
# ----------------------------------------------------------
# 결측치 제거를 한 데이터를 가져와서 진행
df_outlier_std = df_dropna.copy()
mean_num1, std_num1 = df_outlier_std['num1'].mean(), df_outlier_std['num1'].std()
mean_num2, std_num2 = df_outlier_std['num2'].mean(), df_outlier_std['num2'].std()

# 임계값 설정: ±3σ 사이의 범위가 정상 (벗어나면 이상, σ는 표준편차)
lower_num1, upper_num1 = mean_num1 - 3*std_num1, mean_num1 + 3*std_num1
lower_num2, upper_num2 = mean_num2 - 3*std_num2, mean_num2 + 3*std_num2

df_outlier_std = df_outlier_std[
    (df_outlier_std['num1'] >= lower_num1) & (df_outlier_std['num1'] <= upper_num1) &
    (df_outlier_std['num2'] >= lower_num2) & (df_outlier_std['num2'] <= upper_num2)
]

# (4-2) IQR 기반
df_outlier_iqr = df_dropna.copy()
Q1_num1 = df_outlier_iqr['num1'].quantile(0.25)
Q3_num1 = df_outlier_iqr['num1'].quantile(0.75)
IQR_num1 = Q3_num1 - Q1_num1

Q1_num2 = df_outlier_iqr['num2'].quantile(0.25)
Q3_num2 = df_outlier_iqr['num2'].quantile(0.75)
IQR_num2 = Q3_num2 - Q1_num2

low_num1 = Q1_num1 - 1.5*IQR_num1
up_num1  = Q3_num1 + 1.5*IQR_num1
low_num2 = Q1_num2 - 1.5*IQR_num2
up_num2  = Q3_num2 + 1.5*IQR_num2

df_outlier_iqr = df_outlier_iqr[
    (df_outlier_iqr['num1'] >= low_num1) & (df_outlier_iqr['num1'] <= up_num1) &
    (df_outlier_iqr['num2'] >= low_num2) & (df_outlier_iqr['num2'] <= up_num2)
]

print(f"=== [이상치 제거 전 shape] : {df_dropna.shape}")
print(f"=== [표준편차 기준 제거 후 shape] : {df_outlier_std.shape}")
print(f"=== [IQR 기준 제거 후 shape] : {df_outlier_iqr.shape}")
print()

# 정규분포를 가정한다면 표준편차를 활용한 제거가 낫겠으나, 그런 가정이 힘들다면 IQR 방식제거 선택!

 

3. 스케일링

# ----------------------------------------------------------
# 5) 스케일링: 표준화(StandardScaler), 정규화(MinMaxScaler)
#    - 예시로 df_outlier_iqr 를 사용
# ----------------------------------------------------------
# 위에서 iqr 방식으로 이상치를 제거한 데이터를 가져와서 진행
df_scaled = df_outlier_iqr.copy()

# 표준화(StandardScaler)
scaler_std = StandardScaler()

df_scaled['num1_std'] = scaler_std.fit_transform(df_scaled[['num1']])
df_scaled['num2_std'] = scaler_std.fit_transform(df_scaled[['num2']])

# 정규화(MinMaxScaler)
scaler_minmax = MinMaxScaler()

df_scaled['num1_minmax'] = scaler_minmax.fit_transform(df_scaled[['num1']])
df_scaled['num2_minmax'] = scaler_minmax.fit_transform(df_scaled[['num2']])

print("=== [스케일링 결과 컬럼 확인] ===")
print(df_scaled[['num1','num1_std','num1_minmax','num2','num2_std','num2_minmax']].head())
print()

🔍 StandardScaler : 평균이 0, 표준편차가 1 근처로 나오는지 확인

# StandardScaler : 평균이 0, 표준편차가 1 근처로 나오는 것을 확인 => 표준화가 제대로 진행됨
df_scaled[['num1_std', 'num2_std']].describe()

 

🔍 MinMaxScaler : 최소가 0, 최대가 1 인지 확인

# MinMaxScaler : 최소가 0, 최대가 1 로 나옴 => 정규화가 제대로 진행됨
df_scaled[['num1_minmax', 'num2_minmax']].describe()

 

4. 범주형 데이터 변환 (예외 : CatBoost)

# ----------------------------------------------------------
# 6) 범주형 데이터 변환 (원-핫, 라벨 인코딩)
#    - 라벨 인코딩: cat_col
#    - 원-핫 인코딩: cat_col (또는 라벨 인코딩 후 다른 DF에 적용 가능)
# ----------------------------------------------------------

df_cat = df_scaled.copy()

# (6-1) 라벨 인코딩 (예: 학점, 사이즈 등 순서가 있는 데이터에 사용)
label_encoder = LabelEncoder()
df_cat['cat_label'] = label_encoder.fit_transform(df_cat['cat_col'])

# (6-2) 원-핫 인코딩
df_cat = pd.get_dummies(df_cat, columns=['cat_col'])

print("=== [라벨 인코딩 + 원핫 인코딩 결과 컬럼] ===")
print(df_cat.head())
print()

💡 cat_col -> cat_col_A / cat_col_B / cat_col_C 이렇게 3개가 새 변수로 만들어짐 (True=1, False=0)

변수가 3개일 때, 하나를 줄여도 문제가 안됨!

 

5. 파생변수 생성

# ----------------------------------------------------------
# 7) 파생변수 생성
#    - (7-1) 날짜 파생: 연, 월, 요일, 주말여부 등
#    - (7-2) 수치형 변수 조합
#
# ----------------------------------------------------------

df_feat = df_cat.copy()

# (7-1) 날짜 파생
df_feat['year'] = df_feat['date_col'].dt.year
df_feat['month'] = df_feat['date_col'].dt.month
df_feat['dayofweek'] = df_feat['date_col'].dt.dayofweek  # 월=0, 화=1, ...
df_feat['is_weekend'] = df_feat['dayofweek'].apply(lambda x: 1 if x>=5 else 0)

# (7-2) 수치형 변수 조합 예시: num1 + num2
df_feat['num_sum'] = df_feat['num1_std'] + df_feat['num2_std']

print("=== [파생 변수 생성 결과] ===")
print(df_feat[['date_col','year','month','dayofweek','is_weekend','num1','num2','num_sum']].head())
print()

 

6. 다중공선성 확인

# ----------------------------------------------------------
# 8) 다중공선성 확인 (상관관계, VIF)
#    - 예시로 수치형 변수(num1, num2, num_sum, num1_log 등)만 확인
# ----------------------------------------------------------

df_corr = df_feat[['target', 'num1_std', 'num2_std', 'cat_label', 'year', 'month', 'dayofweek',	'is_weekend', 'num_sum']].dropna()
print("=== [상관계수] ===")
print(df_corr.corr())

# VIF 계산 함수
def calc_vif(df_input):
    vif_data = []
    for i in range(df_input.shape[1]):
        vif = variance_inflation_factor(df_input.values, i)
        vif_data.append((df_input.columns[i], vif))
    return pd.DataFrame(vif_data, columns=['feature','VIF'])

vif_df = calc_vif(df_corr)
print("\n=== [VIF 결과] ===")
print(vif_df)
print()

# year는 모든 데이터가 동일한 연도여서 nan으로 나타남

🧐 target(종속변수) 없이 독립변수만 상관관계를 확인!

df_corr.corr()
# 원래는 target(종속변수)없이 독립변수만 상관관계를 확인한다. 강의에서 편하게 다 보여주기 위해 같이 결과를 냄

🚨 num1_std, num2_std, num_sum 은 서로 연관되어 있기 때문에 무한대로 나옴 (num1_std + num2_std = num_sum 관계)

 

7. 불균형 데이터 처리

# ----------------------------------------------------------
# 9) 불균형 데이터 처리: SMOTE
#    - 타깃이 [0,1]로 되어 있고, 1이 매우 적은 상태
#    - SMOTE 적용 위해선 '피처'와 '타깃' 분리 필요
# ----------------------------------------------------------

df_smote = df_feat.copy().dropna(subset=['target'])  # 일단 이상치,결측 제거된 DF 사용

# 독립변수와 종속변수를 분리!!
X = df_smote[['num_sum', 'cat_label', 'year', 'month', 'dayofweek', 'is_weekend']]  # 간단히 수치형 2개만 피처로
y = df_smote['target']   

# ⭐️범주, 클래스, 항목 등을 예측할 때, SMOTH 같은것을 사용함 (연속적인 수치형인 경우, SMOTH 사용X : 분류 -> 정상/이상, 양성/음성 구분)
# 매출값 같은 경우에는 균등한지 아닌지 판단하기 쉽지 않음, 자유롭기 때문에, SMOTH 사용X

print("=== [SMOTE 전 레이블 분포] ===")
print(y.value_counts())

sm = SMOTE(random_state=42)
X_res, y_res = sm.fit_resample(X, y)

print("=== [SMOTE 후 레이블 분포] ===")
print(pd.Series(y_res).value_counts())
print()

✔️ 결과 확인

 

끝.