본문 바로가기
머신러닝

[실무에 쓰는 머신러닝 기초] 2. 데이터 전처리

by 쿙이콩 2025. 7. 1.
728x90

2-1. 데이터 전처리 개요

1. 데이터 전처리란

원시(raw) 데이터에서 불필요하거나 손실(노이즈)이 있는 부분을 처리하고, 분석 목적에 맞는 형태로 만드는 과정

2. 필요성

  1. 모델 정확도 및 신뢰도 향상
  2. 이상치나 결측치가 많은 상태로 학습하면 예측 성능이 크게 떨어짐
  3. 효율적인 데이터 분석과 모델 훈련을 위해 필수적인 단계

3. 사례

제조업
  • 센서가 간헐적으로 측정에 실패해 결측값이 발생
  • 센서 오작동으로 인해 극단적으로 큰 값(이상치)이 기록
  • 정상 제품과 불량 제품의 데이터 분포가 매우 다름(불균형 데이터)
금융
  • 증권사나 은행에서 고객 정보 일부가 유실되거나, 특정 시점의 주가나 거래량 데이터가 취합되지 않은 경우 결측값 발생
  • 특정 종목에 대해 드물게 발생하는 급등락(‘Flash Crash’), 단일 대량 거래에 따른 비정상적 가격 변동 이상치 기록
  • 카드사 사기(부정 거래) 데이터(정상 거래에 비해 ‘부정 거래’는 비율이 극도로 작음)나 대출 연체(default) 사례 데이터의 경우 정상 데이터에 비해 극소수
마케팅
  • 온라인 설문이나 쿠폰 사용 정보가 중간에 누락되거나, 특정 채널(이메일·SNS·오프라인)에서 전송된 쿠폰 수 확인 불가한 경우 결측값 발생
  • 광고 클릭률 중 특정 상품의 노출수/클릭수가 월등히 높아 평균을 왜곡, 혹은 이벤트가 끝나자마자 조회수가 급감하는 경우 이상치 기록
  • 고객 이탈 예측 시 ‘이탈 고객’ 비율이 매우 적은 경우, 특정 광고 캠페인 전환 성공/실패 비율의 극단적 차이가 발생하는 경우 불균형 발생

 

 

 

 


2-2. 결측치 처리

1. 원인

  • 센서 고장, 측정 오류, 환경적 문제 등
  • 사람이 수기로 입력하는 경우 누락

2. 처리 기법

1️⃣ 삭제 removal 2️⃣ 대체 imputation
결측치가 있는 행(row) / 열(column)을 제거
- 간단 but 데이터 손실 발생
- 결측치가 전체 데이터에서 매우 소수일 때 적합
✔️ 평균 또는 중앙값
수치형 데이터에서 많이 사용
데이터 분포 왜곡 비교적 적음

✔️최빈값 범주형 데이터에서 사용

✔️예측 모델로 대체 회귀/분류 모델을 이용해 결측값 예측

3. 예시

제조업 센서 데이터에서 특정 시간대
(야간)에 종종 측정 누락이 발생
1) 중앙값
2) 인접 시간대 센서 값의 추세(ex: 이동평균)로 결측치 채움
3) 중요한 센서라면, 예측 모델을 통해 결측치를 추정
금융


시간에 따라 변하는
시계열 데이터의 경우
(주가, 거래량 등)
1) 일정 기간(예: 이전 5일, 이후 5일 등) 주가 평균 / 가중평균으로 결측값 보정
2) 직전, 직후에 기록된 유효한 데이터로 빈 값 채우기 (Forward/Backward Fill)
고객 데이터(신용도, 소득, 자산 등)에 결측치 발생 1) 중앙값 - 극단값의 영향을 받지 않아 평균 대체보다 안정적
2) 그룹(클러스터)의 대표값으로 대체 (결측값이 속한 클러스터의 평균, 중앙값 등을 사용)
결측값이 간헐적으로 존재 비교적 중요도가 낮은 지표라면, 간단히 제거
마케팅


캠페인 반응(클릭률, 전환율)
데이터의 경우
1) 온라인 광고나 캠페인 전환율이 일시적으로 누락된 경우, 인접 기간(예: 전일, 전주 동일 요일) 데이터 기반으로 보정
2) 시점별로 전환 흐름이 비교적 일정하거나 특정 패턴(요일 효과 등)이 있다면, 이동평균으로 추정
고객 설문/프로필 정보의 경우
1) 나이, 지역, 성별 등의 간단한 인구통계학적 정보는 최빈값·중앙값으로 대체 가능
2) 그룹(클러스터)의 대표값으로 대체 (결측값이 속한 클러스터의 평균, 중앙값 등을 사용)
정기적으로 누락되는 채널/시점 데이터의 경우
1) 특정 채널 또는 시간대에 반복적으로 누락된다면, 시스템(로그 수집, 쿠폰 트래킹 등)을 개선해 재발을 방지
2) 결측이 반복되는 구간은 유사 채널 지표로 추정

4. 파이썬 실습

import numpy as np
import pandas as pd

1) 가상 데이터 생성 - 일부 값들을 np.nan으로 지정해 결측값을 만듭니다.
data = {
    'A': [1, 2, np.nan, 4, 5, np.nan, 7],
    'B': [5, 4, 2, np.nan, np.nan, 3, 1],
    'C': [2, np.nan, np.nan, 6, 7, 8, 9]
}
df = pd.DataFrame(data)
df

2) 결측치 제거 (결측이 하나라도 있으면 해당 행을 제거)
df_drop = df.dropna() ★ axis = 0 (행) / axis = 1 (열)
df_drop

3) 평균값으로 대치
df_mean = df.copy()
df_mean = df_mean.fillna(df_mean.mean(numeric_only=True))
df_mean

4) 중앙값으로 대치 ★ 이상치 있다면 평균보단 중앙값
df_median = df.copy() 
df_median = df_median.fillna(df_median.median(numeric_only=True))
df_median

5) 최빈값으로 대치 ★ mode는 iloc(몇 번째로 채울건지) 해줘야 함
- DataFrame의 mode()는 각 열별로 최빈값을 반환합니다.
- mode() 결과가 여러 개(동률)일 경우 첫 번째 행의 값을 취합니다.
df_mode = df.copy()
print(df_mode.mode()) # 확인용
mode_values = df_mode.mode().iloc[0]  ★첫 번째 행(가장 상위(가장 최빈값) mode)만 취함
df_mode = df_mode.fillna(mode_values)
df_mode
더보기
더보기

df.copy() 원본 df를 그대로 복사하여 새로운 변수에 할당합니다.

- - - 결측치를 채워넣은 결과(DataFrame)가 원본 데이터에 영향을 주지 않도록 별도의 사본을 만들어 작업하기 위함

 

df_mean = df_mean.fillna(df_mean.mean(numeric_only=True))

- - - fillna()는 결측치(NaN)를 주어진 값으로 대체하는 메서드

- - - df_mean.mean(numeric_only=True)는 각 열(column)별 평균값을 계산

- - - numeric_only=True는 숫자형 열에 대해서만 평균을 계산하겠다는 옵션

 

df_median = df_median.fillna(df_median.median(numeric_only=True))

- - - df_median.median(numeric_only=True)는 각 열별 중앙값(median)을 계산

- - - fillna()에 이 중앙값을 전달해, 결측값을 열별 중앙값으로 대체

 

df_mode.mode()

- - - 각 열(column)마다 최빈값(mode)을 찾음

- - - df_mode.mode() 의 결과가 아래와 같이 나왔을 때, .iloc[0]는 첫 번째 행, 즉 A=5.0, B=3.0, C=2.0을 의미

       A    B    C
 0  5.0  3.0  2.0
 1  7.0  3.0  2.0

 

 

 


2-3. 이상치 탐지 및 제거

1. 이상치 outlier 정

  • 정상 범주에서 크게 벗어나는 값
  • 장비 오작동, 환경적 특이사항 등 원인이 다양

2. 탐지 기법

1️⃣ 통계적 기법 (3σ Rule) 2️⃣ 박스플롯(Boxplot) 기준
✔️ 가정  데이터가 정규분포를 따른다
✔️ 이상치 평균에서 ±3σ(표준편차) 범위 벗어나는 값 (약99%임)
✔️ 장단점 직관적이고 간단 / 정규성 가정이 틀릴 수 있음
✔️ 이상치
사분위수(IQR = Q3 - Q1)를 이용,
(Q1 - 1.5×IQR) , (Q3 + 1.5×IQR) 벗어나는 데이터
✔️ 장점 분포 특성에 영향 안 받음
3️⃣ 머신러닝 기반
✔️ 이상치 탐지 알고리즘 (Isolation Forest, DBSCAN 등)
✔️ 복합적 패턴을 고려할 수 있음

3. 처리 기법

1️⃣ 이상치를 단순 제거(필요하다면)

2️⃣ 이상치 값을 조정(클리핑, Winsorizing 등)

3️⃣ 별도로 구분하여 모델에서 제외하거나, 다른 모델(이상치 예측 모델)로 활용

 

4. 예시

제조업
  • 특정 센서값이 정상 범위(ex: 20~80)에서 갑자기 200 이상을 기록 → 장비 이상 가능성 높음
  • 매 생산 라인마다 센서값 범위가 조금씩 다를 수 있으므로, 라인별 통계 기준(평균, 표준편차)으로 이상치 판단
금융
  • 특정 종목의 하루 거래량이 평소 대비 수십 배로 급등하거나, 주가가 순간적으로 비정상적인 폭락·폭등을 보일 때 이상치로 판단
  • 고객 신용도 데이터에서도 소득이나 부채가 일반 범위에서 크게 벗어난 값이 나오면 의심 거래나 데이터 오류로 판단
마케팅
  • 어떤 광고 캠페인에서 클릭률(CTR)이 다른 캠페인에 비해 극단적으로 높아, 평균 분석을 왜곡하는 경우가 이상치
  • 고객 구매 이력에서 특정 기간에 평소와 전혀 다른 과도한 소비(장바구니 금액 폭증)가 포착되면 마케팅 분석에서 이상치로 분류 가능

5. 파이썬 실습

import pandas as pd
import numpy as np

예시 데이터프레임 생성
np.random.seed(42)  # 재현성을 위해 시드 설정
normal_values = np.random.normal(loc=50, scale=5, size=30)   # 평균 50, 표준편차 5인 정규분포에서 30개 값 생성
outliers = [150, 180, 200, 300]  # 눈으로 봐도 이상치로 판단될 수 있는 큰 값들

normal_values와 outliers를 합쳐서 하나의 리스트로 구성
all_values = np.concatenate([normal_values, outliers])

예시로 0~39 범위의 임의 날짜/시간 데이터를 간단히 만들기
dates = pd.date_range('2021-01-01', periods=len(all_values), freq='D')

df = pd.DataFrame({
    'date': dates,
    'sensor_value': all_values
})
df


이상치 제거 (간단하게 박스플롯 기준 적용 예시)
Q1 = df['sensor_value'].quantile(0.25)
Q3 = df['sensor_value'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
df = df[(df['sensor_value'] >= lower_bound) & (df['sensor_value'] <= upper_bound)]
df

 

 


2-4. 정규화 / 표준화

1. 왜 필요한가

  • 모델(특히 거리 기반 알고리즘, 딥러닝 등)에 따라 특정 변수의 스케일이 크게 영향을 미칠 수 있음
  • 센서 A는 값 범위가 0~1000, 센서 B는 값 범위가 0~1이라면, A가 모델에 더 큰 영향을 줌

2. 정규화와 표준화

정규화 MinMaxScaler 표준화  StandardScaler
모든 값을 0과 1 사이로 매핑 평균을 0, 표준편차를 1로 만듦
특징
- 값의 스케일이 달라도 공통 범위로 맞출 수 있음
- 딥러닝(신경망), 이미지 처리 등에서 입력값을 0~1로 제한 / 각 특성이 동일한 범위 내 있어야 하는 경우 자주 사용
- 거리 기반 알고리즘(유클리디안 거리 사용) / 각 특성의 범위를 동일하게 맞춤으로써 계산 안정성을 높이고 싶을 때
- 단점 최소값·최대값이 극단값(Outlier)에 민감 - 만약 극단치가 있으면 대부분의 데이터가 [0, 1] 구간 내부 한쪽에 치우침
- 새로운 데이터가 기존 최대값보다 커지거나,
  최소값보다 작아지는 경우,
  스케일링 범위를 벗어날 수 있어 재학습하거나 다른 처리가 필요
특징
- 분포가 정규분포에 가깝게 변형
- 평균이 0, 표준편차가 1로 맞춰지므로, 정규분포 가정을 사용하는
  알고리즘(선형회귀, 로지스틱회귀, SVM 등)에 자주 쓰임
- 변환된 값들이 이론적으로 -∞ ~ +∞ 범위를 가질 수 있음
- - - 그러나 대부분 -3 < 값 < +3 사이에 위치함
- 데이터가 특정 구간([0, 1] 등)에 고정 X
- 데이터 분포가 심하게 치우쳐 있으면, 평균과 표준편차만으로는 
  충분한 스케일링이 되지 X
- - -(로그 변환, RobustScaler 등 추가 고려).

좌) 정규화 / 우) 표준화

3. 파이썬 실습

import pandas as pd
import numpy as np

난수를 재현하기 위해 시드 설정
np.random.seed(42)

예시 마케팅 지표 데이터 생성
data_size = 10
df = pd.DataFrame({
    'impressions': np.random.randint(1000, 10000, size=data_size), # 광고 노출 횟수
    'clicks': np.random.randint(0, 300, size=data_size), # 광고 클릭 횟수
    'conversions': np.random.randint(0, 50, size=data_size), # 광고를 통해 구매한 횟수
    'cost': np.random.randint(100, 5000, size=data_size), # 광고비 지출액
    'revenue': np.random.randint(100, 10000, size=data_size) # 광고를 통해 발생한 매출
})
df


from sklearn.preprocessing import MinMaxScaler

스케일링을 적용할 컬럼만 선정
cols_to_scale = ['impressions', 'clicks', 'conversions', 'cost', 'revenue']

MinMaxScaler 객체 생성(기본 스케일: [0,1])
minmax_scaler = MinMaxScaler()

fit_transform을 통해 스케일링된 결과를 데이터프레임으로 변환
df_minmax_scaled = pd.DataFrame(minmax_scaler.fit_transform(df[cols_to_scale]), 
                                columns=cols_to_scale)                            
print(df_minmax_scaled.max())
print(df_minmax_scaled.min())
                     
df_minmax_scaled

from sklearn.preprocessing import StandardScaler

StandardScaler 객체 생성
standard_scaler = StandardScaler()

fit_transform을 통해 스케일링된 결과를 데이터프레임으로 변환
df_standard_scaled = pd.DataFrame(standard_scaler.fit_transform(df[cols_to_scale]), 
                                  columns=cols_to_scale)

print(df_standard_scaled.mean())
print(df_standard_scaled.std())
df_standard_scaled
더보기
더보기

MinMaxScaler()  MinMaxScaler()를 만들어서 scaler라는 이름으로 할당

fit_transform(df [ [ '칼럼이름' ] ] ) 호출

1️⃣ fit: 열을 살펴 최솟값과 최댓값을 찾습니다.

2️⃣ transform: 찾은 최소·최대값으로 각 데이터를 0~1 범위로 바꿉니다.

 

StandardScaler()StandardScaler()를 만들어서 standard_scaler라는 이름으로 할당

fit_transform(df[['칼럼이름']]) 호출

1️⃣ fit: 열의 평균과 표준편차를 구합니다.

2️⃣ transform: 각 값을 (x - 평균) / 표준편차로 변환해, 평균 0, 표준편차 1인 분포를 만듭니다.

 

 

 


2-5. 불균형 데이터 처리

1. 불균형 데이터란

class imbalance = 정상 99%, 불량 1%처럼 한 클래스가 극도로 적은 경우

2. 문제점

모델이 극도로 적은 클래스를 거의 예측하지 못할 가능성이 큼(편향 발생)

3. 해결 기법

1️⃣ Oversampling 2️⃣ Undersampling 3️⃣ 혼합 기법
✔️ Random Oversampling
소수 클래스의 데이터를 단순 복제하여 개수를 늘림

✔️  SMOTE
(Synthetic Minority Over-sampling Technique)
- 소수 클래스를 "무작정 복사" X “비슷한” 데이터들을 서로 섞어서(Interpolation) 새로운 데이터 생성 O
- 소수 클래스(ex: 스팸) 안에서 가까운 데이터 둘(혹은 몇 개)을 고르고, 그 사이에 새 데이터 포인트를 만들어내어, 소수 클래스의 다양한 예시를 가상으로 늘리는 기법
- 예시
→ 오렌지(소수 클래스)와 사과(다수 클래스)가 있는 과일 바구니
→ 사과는 90개, 오렌지는 10개 = 사과 훨씬 많음
→ "오렌지를 조금 더 만들어서(복사)" 갯수를 맞출 수도 있지만, 그러면 똑같은 오렌지가 여러 개 생길 뿐, 다양성이 없음
→ SMOTE = “모양이나 맛이 비슷한 두 오렌지를 고른 다음, 그 중간 정도 되는 새로운 오렌지를 상상해서 만들어낸다” 느낌
  기존 오렌지랑 똑같지도 않고, 완전히 엉뚱하지도 않은 새 오렌지를 얻을 수 있음

- 다수 클래스 데이터를 줄이는 방식
- 데이터 손실 위험이 있지만, 전체 데이터 균형을 맞출 수 있음
 - SMOTE와 언더샘플링을 적절히 섞어서 사용

4. 파이썬 실습

import numpy as np
import pandas as pd

난수 고정 (재현성)
np.random.seed(42)

불균형 데이터 크기 설정
예: 총 100개 중 defect=1(불량)인 샘플 10개, defect=0(정상)인 샘플 90개
size_1 = 10
size_0 = 90

정상 클래스 (defect=0) 데이터 생성
feature1_0 = np.random.normal(loc=10, scale=2, size=size_0)
feature2_0 = np.random.normal(loc=5, scale=1, size=size_0)

불량 클래스 (defect=1) 데이터 생성
feature1_1 = np.random.normal(loc=20, scale=5, size=size_1)
feature2_1 = np.random.normal(loc=10, scale=2, size=size_1)

배열 병합
feature1 = np.concatenate([feature1_0, feature1_1])
feature2 = np.concatenate([feature2_0, feature2_1])
defect = np.array([0]*size_0 + [1]*size_1)

데이터프레임 생성
df = pd.DataFrame({
    'feature1': feature1,
    'feature2': feature2,
    'defect': defect
})

df

df.hist()  ★ 0은 90개, 1은 10개 나옴

from imblearn.over_sampling import SMOTE
# 불균형 데이터 처리 (SMOTE)
X = df.drop('defect', axis=1)   # 결측치 처리, 이상치 제거, 인코딩 등 사전 처리 후
y = df['defect']
★ defect(Y)가 불균형한지 확인하고 싶기 때문에, defect제외한 axis만 X
smote = SMOTE(random_state=42)
X_res, y_res = smote.fit_resample(X, y)

X_res
y_res
y_res.hist() ★ 결과, X도 90개, Y도 90개, 총 180개가 됨(100개로 시작했었음)
더보기
더보기

X = df.drop('defect', axis=1)

- df 데이터프레임에서 defect 컬럼(레이블)을 제외한 나머지를 X(특징 행렬)로 사용

- 이때, 이미 결측치 처리, 이상치 제거, 범주형 인코딩 등의 사전 전처리를 마쳤다고 가정

 

y = df['defect']

- defect 컬럼을 타겟(레이블)으로 설정

- 예) defect가 1이면 제품 결함 있음, 0이면 결함 없음 등

 

smote = SMOTE(random_state=42)

- SMOTE는 소수 클래스(예: 결함 사례)가 너무 적을 때, 기존 소수 클래스 데이터들을 바탕으로 유사한 새로운 예시를 만들어 데이터 개수를 늘려주는 기법

- random_state=42는 재현성(코드 실행 시 동일 결과)을 위해 난수 시드를 고정하는 역할

- SMOTE 객체를 생성

 

- X_res, y_res = smote.fit_resample(X, y)

- fit_resample을 통해 SMOTE 알고리즘이 X, y를 바탕으로 소수 클래스 데이터를 자동 생성

- 결과적으로, 오버샘플링된 X_res, y_res에는 클래스 불균형이 개선된(1:1에 가깝거나 원하는 비율이 된) 상태가 됨

 

 


2-6. 범주형 데이터 변환

1. 원-핫 인코딩 / 레이블 인코딩

원-핫 인코딩(One-Hot Encoding) 레이블 인코딩(Label Encoding)
  • 범주형 변수를 각각의 범주별로 새로운 열로 표현, 해당 범주에 해당하면 1, 아니면 0
  • ex) 색상(‘Red’, ‘Blue’, ‘Green’) → ‘Red=1,Blue=0,Green=0’ / ‘Red=0,Blue=1,Green=0’ / …
  • 장점: 범주 간 서열 관계가 없을 때 사용하기 좋음
  • 단점: 범주가 매우 많으면 차원이 커짐 (= 컬럼이 많아짐)
    - 차원 = 컬럼 개수임
  • 범주를 숫자로 직접 맵핑(‘M’=0, ‘L’=1, ‘XL’=2 등)
  • 단순하지만, 모델이 숫자의 크기를 서열 정보로 잘못 해석할 수 있음

2. 파이썬 실습 (원-핫 인코딩)

import pandas as pd
import numpy as np

예시 데이터프레임 생성
data_size = 10
np.random.seed(42)

labels = ['apple', 'banana', 'cherry']
random_labels = np.random.choice(labels, data_size)

df = pd.DataFrame({
    'id': range(1, data_size + 1),
    'label': random_labels,
    'value': np.random.randint(1, 100, data_size),
    'another_feature': np.random.choice(['A', 'B'], data_size)  # 또 다른 범주형 변수
})

df ★ True = 1 / False = 0

df['label'].unique()
★ 중복 제외 범주형 레이블 뭐 있나 확인
결과) array(['cherry', 'apple', 'banana'], dtype =object)

범주형 변수 변환 (원-핫 인코딩 예시)
df = pd.get_dummies(df, columns=['label'])
더보기
더보기

pd.get_dummies(df, columns=['칼럼이름'])

→열의 범주들(A, B, C 등)을 각각 별도 열로 만들어, 해당하는 행에는 1, 그렇지 않은 행에는 0을 넣어줌

 

3. 파이썬 실습 (레이블 인코딩)

import pandas as pd
import numpy as np

# 예시 데이터프레임 생성
data_size = 10
np.random.seed(42)

labels = ['apple', 'banana', 'cherry']
random_labels = np.random.choice(labels, data_size)

df = pd.DataFrame({
    'id': range(1, data_size + 1),
    'label': random_labels,
    'value': np.random.randint(1, 100, data_size),
    'another_feature': np.random.choice(['A', 'B'], data_size)  # 또 다른 범주형 변수
})

df

# 범주형 변수 변환 (레이블 인코딩 예시)
from sklearn.preprocessing import LabelEncoder
encoder = LabelEncoder()
df["label"] = encoder.fit_transform(df["label"])
df
더보기
더보기

from sklearn.preprocessing import LabelEncoder

사이킷런(sklearn) 라이브러리에서 LabelEncoder 클래스를 불러옴

LabelEncoder는 문자열이나 범주형 데이터를 정수로 변환하기 위한 클래스

 

encoder = LabelEncoder()

LabelEncoder를 인스턴스화하여 encoder라는 이름으로 객체를 생성

 

df["label"] = encoder.fit_transform(df["label"])

- df["label"] 열(column)을 fit_transform 메서드에 전달

  • fit: 데이터에 등장하는 범주를 학습
  • transform: 학습한 매핑에 따라 데이터를 정수로 변환

변환된 결과(정수 라벨)를 다시 df["label"]에 덮어쓰기

 

예를 들어, ["red", "blue", "blue", "green"] 같은 문자열 범주가 존재하면,

  • "blue" → 0
  • "green" → 1
  • "red" → 2와 같이 매핑 (실제 순서는 데이터에 따라 달라짐)




2-7. 피처 엔지니어링 개요

모델 성능 향상을 위해 기존 데이터를 변형, 조합하여 새로운 특성(피처)을 만드는 작업

 

1. 중요성

  • 복잡한 데이터 구조 안에 존재하는 패턴을 효과적으로 추출해 모델이 쉽게 학습하게 함
  • 제조업에서는 센서 데이터 간 시계열적·물리적 관계를 반영하는 경우가 많음
    금융에서의 데이터는 고객 신용도, 거래내역, 시장 지표 등 복잡하고 다양한 변수를 포함하는 경우가 많음
    마케팅에서는 고객 행동 데이터(클릭, 구매 기록, 웹사이트 체류 시간 등)와 고객 특성 데이터(나이, 지역, 관심 분야 등)를 통합해 피처를 만들어야 효과적인 캠페인 타깃팅, 고객 세분화, 개인화 추천이 가능한 경우가 많음

2. 실습 예시

* 차원축소 (변형해서 축소) - 변수 선택 (그냥 추출) : 둘은 다름

파생 변수 생성 1) 날짜 파생 변수
ex) 측정 시간이 ‘2025-02-24 10:35:00’이라면, ‘월(2)’, ‘요일(월=1)’, ‘시(10)’, ‘주말여부(0/1)’ 등으로 분해

2) 수치형 변수 조합
ex) ‘온도’와 ‘습도’가 있을 때, 새로운 피처 ‘온도×습도(TEMP×HUMID)’를 추가
두 변수의 상호작용이 불량 발생에 영향을 줄 수 있음

3) 로그 변환, 제곱근 변환 등
분포가 매우 치우친 변수(오른쪽 꼬리가 긴 경우)에 로그 변환을 적용하여 정규성에 가까워지도록 조정
변수 선택 1) 상관관계
변수 간 상관도가 높은 상황인 경우 다중공선성 의심. 중복 정보가 클 수 있으므로, 하나만 남기거나 둘 다 제거 고려  - - -  다중공선성 

2) VIF
VIF는 어떤 변수 하나가, 다른 변수들과 얼마나 겹치는지(상관이 큰지) 수치로 보여주는 지표
회귀분석에서 다중공선성 문제를 파악할 때 사용
VIF가 일정 기준(예: 10 이상)을 넘으면 해당 변수를 제거하거나 비슷한 변수들을 합치는(변환) 등의 방법으로 문제를 해결

3) 모델 기반 중요도(Feature Importance)
트리 기반 모델(랜덤 포레스트, XGBoost 등)을 훈련 후 중요도가 낮은 변수를 제거
변수 간 상호작용 추가 다항식/교차 항 생성 ex) 2차 다항식(Quadratic Features)
제조 공정에서 온도, 압력, 속도 등이 곱해져야 비로소 의미가 생기는 경우가 많음

 

파생변수 생성 코드 예시

import pandas as pd
import numpy as np

np.random.seed(42)  # 재현성을 위한 시드 고정

# 10개 데이터 샘플 생성
data_size = 10

# 날짜/시간 컬럼(예시)
dates = pd.date_range(start="2023-01-01", periods=data_size, freq='D')

# 온도(°C) : 15 ~ 35 사이 정수
temperature = np.random.randint(15, 36, size=data_size)

# 습도(%) : 30 ~ 90 사이 정수
humidity = np.random.randint(30, 91, size=data_size)

df = pd.DataFrame({
    'date': dates,
    'temperature': temperature,
    'humidity': humidity
})

df

# 피처 엔지니어링 (온도와 습도 간 상호작용)
df['temp_humid_interaction'] = df['temperature'] * df['humidity']
df




2-8. 코드 실습

★패키지 불러오기
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
----------------------------------------------------------
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
})
----------------------------------------------------------
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()
----------------------------------------------------------
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()
----------------------------------------------------------
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()
----------------------------------------------------------
5) 스케일링: 표준화(StandardScaler), 정규화(MinMaxScaler)
   - 예시로 df_outlier_iqr 를 사용
----------------------------------------------------------

df_scaled = df_outlier_iqr.copy()

scaler_std = StandardScaler()
scaler_minmax = MinMaxScaler()

df_scaled['num1_std'] = scaler_std.fit_transform(df_scaled[['num1']])
df_scaled['num2_std'] = scaler_std.fit_transform(df_scaled[['num2']])
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()
----------------------------------------------------------
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()
----------------------------------------------------------
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()
----------------------------------------------------------
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()
----------------------------------------------------------
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']

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()
728x90