1. 오늘배운것
데이터 불균형 : 정상 범주의 관측치 수와 이상 범주의 관측치 수가 현저히 차이나는 데이터
정상을 정확히 분류하는 것과 이상을 정확히 분류하는 것 중 일반적으로 이상을 정확히 분류하는 것이 더 중요
왜냐하면 보통 이상 데이터가 target값이 되는 경우가 많기 때문
1. 불균형 데이터의 위험
- 정확도 함정: 불균형 데이터로 학습된 모델은 다수 클래스(정상)에 치우친 예측을 할 가능성이 높습니다. 전체 데이터 100% 정상 95% 불량 5%
- 결함 탐지 실패: 소수 클래스(불량)를 충분히 학습하지 못하면 결함 제품을 놓칠 위험이 있습니다. 이는 품질 문제로 이어질 수 있습니다.
2. QA/ QC에서의 목표
- 데이터 균형 유지: 데이터셋을 구축할 때 클래스 균형을 최대한 맞추는 것이 중요합니다.
- 데이터 증강 기법이나 샘플링 기법을 활용하거나, 생산 환경에서 결함 데이터를 의도적으로 추가 수집합니다.
3. QA에서의 해결 방안
- 데이터 수집 전략 강화:
- 소수 클래스(불량) 데이터를 더 많이 수집하는 방안을 마련or 샘플링 기법.
- 생산 환경에서 다양한 불량 케이스를 시뮬레이션하거나 의도적으로 생성.
- 모델 검증 강화:
- 불균형 데이터 상황에서 과소 적합(underfitting)이나 과적합(overfitting)을 방지하기 위한 교차 검증 및 다양한 시나리오 기반 테스트 수행
데이터의 불균형을 해결하는 법
언더 샘플링과 오버 샘플링의 기법

언더 샘플링: 다수 범주의 데이터를 소수 범주의 데이터 수에 맞게 줄이는 샘플링 방식을 말함.
1) Random Sampling Random Sampling : 다수 범주에서 무작위로 샘플링을 하는 것

코드예시)
import numpy as np
import pandas as pd
from sklearn.utils import resample
# 예제 데이터셋 생성
data = {'feature1': np.random.randn(1000), # 랜덤 피처 데이터
'feature2': np.random.randn(1000),
'class': [0] * 900 + [1] * 100} # 클래스 불균형 (0: 900개, 1: 100개)
df = pd.DataFrame(data)
# 클래스 분리
df_majority = df[df['class'] == 0] # 다수 클래스 (0)
df_minority = df[df['class'] == 1] # 소수 클래스 (1)
# 랜덤 언더샘플링
df_majority_downsampled = resample(df_majority,
replace=False, # 복제하지 않음
n_samples=len(df_minority), # 소수 클래스 크기와 동일
random_state=42) # 재현성을 위해
# 언더샘플링 데이터 병합
df_balanced = pd.concat([df_majority_downsampled, df_minority])
print("언더샘플링 전 데이터 분포:")
print(df['class'].value_counts())
print("\n언더샘플링 후 데이터 분포:")
print(df_balanced['class'].value_counts())
2) Tomek Links : 두 범주 사이를 탐지하고 정리를 통해 부정확한 분류경계선을 방지하는 방법
다른 클래스의 데이터 두 개를 연결했을 때 주변에 다른 임의의 데이터 Xk가 존재할 때
선택한 두 데이터에서 Xk까지의 거리보다 선택한 두 데이터 사이의 거리가 짧을 때 선택한 두 데이터 간의 링크를 Tomek Link라 부릅니다.


코드예시)
from imblearn.under_sampling import TomekLinks
import numpy as np
import pandas as pd
# 예제 데이터 생성
np.random.seed(42)
X = np.vstack((np.random.normal(0, 1, (100, 2)), np.random.normal(3, 1, (10, 2)))) # 피처 데이터
y = np.array([0] * 100 + [1] * 10) # 클래스 라벨 (불균형)
# Tomek Link 적용
tomek = TomekLinks(sampling_strategy='auto') # 다수 클래스 데이터만 제거
X_resampled, y_resampled = tomek.fit_resample(X, y)
# 결과 출력
print("Tomek Link 적용 전 데이터 크기:", X.shape)
print("Tomek Link 적용 후 데이터 크기:", X_resampled.shape)
print("원래 클래스 분포:", pd.Series(y).value_counts())
print("Tomek Link 적용 후 클래스 분포:", pd.Series(y_resampled).value_counts())
오버 샘플링: 다수 범주의 데이터를 소수 범주의 데이터 수에 맞게 줄이는 샘플링 방식을 말함.
1) Resampling : 소수 클래스 데이터를 단순 복제하여 데이터 양을 증가

from sklearn.utils import resample
import pandas as pd
# 데이터 생성
data = {'feature': [1, 2, 3, 4, 5, 6], 'class': [0, 0, 0, 0, 0, 1]} # 클래스 0이 다수
df = pd.DataFrame(data)
# 소수 클래스 분리
df_majority = df[df['class'] == 0]
df_minority = df[df['class'] == 1]
# 랜덤 오버샘플링
df_minority_oversampled = resample(df_minority,
replace=True, # 복제 허용
n_samples=len(df_majority), # 다수 클래스와 동일한 크기로
random_state=42) # 재현성
# 오버샘플링 데이터 병합
df_oversampled = pd.concat([df_majority, df_minority_oversampled])
print("오버샘플링 전 데이터 분포:")
print(df['class'].value_counts())
print("\n오버샘플링 후 데이터 분포:")
print(df_oversampled['class'].value_counts())
2) SMOTE : 소수 범주에서 가상의 데이터를 생성하는 방법
K값을 정한 후 소수 범주에서 임의의 데이터를 선택한 후
선택한 데이터와 가장 가까운 K개의 데이터 중 하나를 무작위로 선정해 Synthetic 공식을 통해 가상의 데이터를 생성하는 방법

from imblearn.over_sampling import SMOTE
import numpy as np
import pandas as pd
# 데이터 생성 (불균형 데이터셋)
X = np.array([[1, 2], [2, 3], [3, 4], [4, 5], [10, 10], [20, 20]]) # 특징 데이터
y = np.array([0, 0, 0, 0, 0, 1]) # 클래스 (불균형 데이터)
print("SMOTE 적용 전 클래스 분포:")
print(pd.Series(y).value_counts())
# SMOTE 초기화 (k_neighbors=2)
smote = SMOTE(k_neighbors=2, random_state=42)
#훈련데이터 테스테로 분할 후에 해야한다.
x_train, x_test
# 데이터 오버샘플링
X_resampled, y_resampled = smote.fit_resample(X_train, y_train)
print("\nSMOTE 적용 후 클래스 분포:")
print(pd.Series(y_resampled).value_counts())
print("\nSMOTE로 생성된 데이터:")
print(X_resampled)
인코딩

코드예시)
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
# 예시 데이터
df = pd.DataFrame({
"color": ["red", "blue", "green", "blue", "red"],
"size": ["S", "M", "L", "M", "L"],
"price": [100, 150, 200, 130, 170]
})
X = df[["color", "size"]]
y = df["price"]
# 1) Train/Test 분리 먼저!
X_train, X_test = train_test_split(X, test_size=0.2, random_state=42)
# 2) Label Encoding (size는 S<M<L 순서가 있음)
le = LabelEncoder()
X_train["size"] = le.fit_transform(X_train["size"]) # Train으로 fit
X_test["size"] = le.transform(X_test["size"]) # Test는 transform만
# 3) One-Hot Encoding (color는 순서 없음)
ohe = OneHotEncoder(handle_unknown="ignore", sparse_output=False)
X_train_color = ohe.fit_transform(X_train[["color"]])
X_test_color = ohe.transform(X_test[["color"]])
print("인코딩 완료")
print(X_train)
print(X_train_color)
스케일링
필요한 이유: 모델이 숫자를 볼 때 스케일 차이 때문에 특정 변수만 중요하다고 착각할 수 있다.

import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
# 예시 데이터 생성
data = {
"price": [100, 150, 200, 130, 170, 210, 190, 160, 180, 140] # 수치형 피처
}
df = pd.DataFrame(data)
# Train/Test 분리
X_train, X_test = train_test_split(df, test_size=0.3, random_state=42)
# StandardScaler 사용
scaler = StandardScaler()
# Train 데이터로 fit하고 transform
X_train_scaled = scaler.fit_transform(X_train[["price"]])
# Test 데이터는 transform만 적용
X_test_scaled = scaler.transform(X_test[["price"]])
print("===== 원본 데이터 (Train) =====")
print(X_train.head())
print("\n===== 표준화 데이터 (Train 결과) =====")
print(X_train_scaled[:5])
print("\n===== 표준화 시 사용된 평균, 표준편차 =====")
print("평균(mean):", scaler.mean_)
print("표준편차(std):", scaler.scale_)
중요한 규칙
✅ Train/Test Split 먼저 하고 → 그 다음 인코딩/스케일링
✅ fit은 Train만 (fit_transform)
✅ Test에는 transform만
✅ SMOTE는 오버샘플링이고, Train ,test 분할 후 Train 에만 적용한다.
Test 정말 우리 모델이 완벽하게 예측하는지 검증하기 위한 데이터셋이기때문에
절대절대 건드려서는 안된다.