인지야공

인지야공/ADP/3번째 글

ADP 실기 — EDA 와 전처리, 손이 먼저 나가야 하는 것들

ADP 실기의 머신러닝 문항은 거의 항상 “탐색적 분석하고, 전처리하고, 그 이유를 쓰시오”로 시작한다. 코드보다 선택의 근거에 점수가 붙는다. 그래서 이 글은 코드 옆에 “왜 그것을 골랐는지”를 같이 적어 둔다.

데이터를 처음 열었을 때

순서대로 친다. 생각하지 않고 친다.

print(df.shape)
print(df.head())
print(df.info())
print(df.describe())
print(df.isnull().sum())

info() 로 타입을, describe() 로 분포를, isnull().sum() 으로 결측을 본다. 이 셋이 전처리 계획 전부를 결정한다.

컬럼을 한 번에 훑는 루프

컬럼이 열 개를 넘어가면 위 출력은 눈에 안 들어온다. 이걸 쓴다.

for col in X.columns:
    print("변수: {}, 타입: {}, 결측 개수: {}, 상태공간 크기: {}, 상태공간 일부: {}".format(
        col, X[col].dtype, X[col].isnull().sum(), len(X[col].unique()), X[col].unique()[:5]))

“상태공간 크기”가 이 루프의 핵심이다. 고유값이 2~10 개면 범주형이고, 수백 개면 연속형이고, 행 수와 같으면 ID 라 버려야 한다. dtype 이 int64 라고 연속형인 게 아니다 — 0/1 로 채워진 int64 를 스케일링하면 아무 의미가 없다.

타깃 분포부터 본다

y.value_counts(normalize=True)     # 비율
y.value_counts()                   # 개수
y.value_counts(normalize=True).plot(kind='bar')

분류 문제라면 여기서 클래스 불균형을 발견해야 한다. 23회는 0 이 12%, 1 이 88% 였고, “불균형을 시각화하여 식별하고 판단 근거를 작성”이 그대로 배점 항목이었다. 발견하고 넘어가는 게 아니라 그래프를 그려서 근거로 남기는 것까지가 답이다.

결측치 — 다섯 가지와 그 근거

21회, 24회 모두 “결측치를 예측하는 방법 2가지를 쓰고 선택한 이유를 설명”이 나왔다. 그래서 다섯 개를 준비하고 상황에 맞춰 둘을 골랐다.

# 1. 평균
df['col'].fillna(df['col'].mean(), inplace=True)

# 2. 중앙값 — 이상치가 있으면 이쪽
df['col'].fillna(df['col'].median(), inplace=True)

# 3. 최빈값 — 범주형
df['col'].fillna(df['col'].mode()[0], inplace=True)
# 4. 회귀 대치 — 다른 변수로 결측을 예측한다
from sklearn.linear_model import LinearRegression

X_train = df[df['col'].notnull()][['col1', 'col2']]
y_train = df[df['col'].notnull()]['col']
X_test = df[df['col'].isnull()][['col1', 'col2']]

lr = LinearRegression().fit(X_train, y_train)
df.loc[df['col'].isnull(), 'col'] = lr.predict(X_test)
# 5. 다중 대치 (MICE)
from sklearn.experimental import enable_iterative_imputer   # 이 줄이 없으면 import 가 실패한다
from sklearn.impute import IterativeImputer

imp = IterativeImputer(max_iter=10, random_state=0)
df_filled = pd.DataFrame(imp.fit_transform(df), columns=df.columns)

전체를 한 번에 밀어 버릴 때는 SimpleImputer 를 쓴다.

from sklearn.impute import SimpleImputer

imputer = SimpleImputer().fit(X_train)
X_train = pd.DataFrame(imputer.transform(X_train), columns=X_train.columns)
X_test  = pd.DataFrame(imputer.transform(X_test),  columns=X_train.columns)

fit 은 train 에만 한다. test 에 fit_transform 을 쓰면 test 의 평균이 대치값에 섞인다. 답안에 한 줄로 적어 두면 그 자체가 점수다.

방법고르는 이유
평균분포가 대칭이고 결측이 적을 때. 분산을 줄인다는 단점을 같이 쓴다
중앙값이상치가 있을 때. 평균은 이상치에 끌려간다
최빈값범주형. 다른 선택지가 없다
회귀결측 변수가 다른 변수와 상관이 뚜렷할 때. 관계를 과장한다는 단점이 있다
다중 대치결측이 많고 여러 변수에 걸쳐 있을 때. 불확실성을 반영한다

이상치

IQR 이 기본이다. 확인 → 제거 또는 대체.

sns.boxplot(df['income'])
plt.show()

Q1 = df['income'].quantile(0.25)
Q3 = df['income'].quantile(0.75)
IQR = Q3 - Q1
outlier = (df['income'] < Q1 - 1.5*IQR) | (df['income'] > Q3 + 1.5*IQR)

df = df[~outlier]                              # 제거
df.loc[outlier, 'income'] = df['income'].mean()  # 또는 대체

데이터가 아깝거나 관측치가 적으면 사분위를 0.05 / 0.95 로 넓게 잡아 극단값만 쳐낸다. RFM 군집 문제(25·26회)에서는 이쪽을 썼다 — 소비 데이터는 꼬리가 원래 길어서 1.5×IQR 로 자르면 상위 고객이 통째로 날아간다.

Q1 = rfm.Amount.quantile(0.05)
Q3 = rfm.Amount.quantile(0.95)
IQR = Q3 - Q1
rfm = rfm[(rfm.Amount >= Q1 - 1.5*IQR) & (rfm.Amount <= Q3 + 1.5*IQR)]

이상치는 “제거”가 정답이 아니다. 26회는 “이상치 제거하는 방법을 설명하고, 제거한 결과를 통계적으로 나타낼 것”이었다. 제거 전후의 describe() 를 나란히 붙이는 게 답이다.

범주형 변수

pd.get_dummies(df, columns=['gender', 'city'], drop_first=True)   # 원-핫
df['Sex'] = df['Sex'].map({'male': 0, 'female': 1})               # 이진 매핑

from sklearn import preprocessing
le = preprocessing.LabelEncoder()
df['category'] = le.fit_transform(df['category'])                 # 레이블
언제
원-핫순서가 없는 범주. 트리 계열이 아닌 모델에는 이쪽이 원칙
레이블순서가 있거나(학력·등급), 범주가 너무 많아 차원이 폭발할 때, 트리 계열 모델

drop_first=True 는 다중공선성 때문이다. 범주가 kk 개면 더미는 k−1k-1 개면 충분하고, kk 개를 다 넣으면 완전 공선성이 생긴다. 회귀에서는 반드시 넣고, 트리에서는 안 넣어도 된다.

스케일링

from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler

scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test  = scaler.transform(X_test)      # test 는 transform 만
언제
StandardScaler평균 0, 분산 1. 기본값
MinMaxScaler0~1. 범위가 정해진 값
RobustScaler중앙값과 IQR. 이상치가 남아 있을 때

거리 기반(KNN·SVM·KMeans)과 경사하강 기반(신경망·로지스틱)은 스케일링이 필수, 트리 계열은 불필요하다. 이 구분을 답안에 쓰면 “왜 스케일링했는가”에 그대로 답이 된다.

로그 변환은 별도다. 17회는 주택 가격 데이터를 log1p 로 정규화하는 문제였다.

from sklearn.preprocessing import FunctionTransformer

log1p_transformer = FunctionTransformer(lambda X: np.log1p(X))
X_norm = log1p_transformer.transform(X)

log 가 아니라 log1p 인 이유는 값에 0 이 섞여 있어서다. log⁡(0)\log(0) 은 −∞-\infty 지만 log⁡(1+0)=0\log(1+0) = 0 이다.

클래스 불균형

22회·23회 모두 “오버샘플링과 언더샘플링을 설명하고 비교한 뒤 하나를 고르고 근거를 제시”였다. 세 가지를 다 돌려 놓고 비교표를 만드는 게 안전하다.

from collections import Counter
from imblearn.over_sampling import SMOTE, RandomOverSampler
from imblearn.under_sampling import RandomUnderSampler

print("적용 전:", Counter(y))

X_res, y_res = SMOTE(random_state=42).fit_resample(X, y)
print("SMOTE 후:", Counter(y_res))

X_res, y_res = RandomUnderSampler(random_state=42, sampling_strategy='majority').fit_resample(X, y)
print("언더샘플링 후:", Counter(y_res))

X_res, y_res = RandomOverSampler(random_state=42, sampling_strategy='minority').fit_resample(X, y)
print("오버샘플링 후:", Counter(y_res))
방식위험
RandomUnderSampler다수 클래스를 버린다정보 손실. 데이터가 적으면 못 쓴다
RandomOverSampler소수 클래스를 복제한다같은 점이 반복돼 과적합
SMOTE소수 클래스 이웃 사이를 보간해 새 점을 만든다클래스가 겹쳐 있으면 경계에 잡음을 만든다

샘플링은 train 에만 한다. test 까지 오버샘플링하면 성능이 부풀려지고, 그건 틀린 답이다. 23회에서 “원데이터 포함 3개 데이터 세트를 구성”하라고 한 것도 결국 이 비교를 시키는 것이다.

표본추출 네 가지

통계 문항에서 개념으로 물을 때가 있어 코드까지 준비해 두었다.

# 1) 단순임의추출
import random
random.sample(population, 3)

from sklearn.model_selection import train_test_split
train_test_split(data, test_size=0.3, random_state=1)
# 2) 계통추출 / 부트스트랩 — 복원추출로 통계량의 분포를 만든다
import numpy as np

def bootstrap(data, n_bootstrap, statistic_fn):
    statistics = []
    n = len(data)
    for _ in range(n_bootstrap):
        sample = np.random.choice(data, n, replace=True)
        statistics.append(statistic_fn(sample))
    return statistics

samples = bootstrap(np.arange(1, 11), n_bootstrap=1000, statistic_fn=np.median)
print("부트스트랩 중앙값 평균:", np.mean(samples))
# 3) 집락추출 — 군집을 만들고 군집 단위로 뽑는다
from sklearn.cluster import KMeans
labels = KMeans(n_clusters=2).fit(X).labels_
# 4) 층화추출 — 층의 비율을 유지한 채 나눈다
train_test_split(DF.iloc[:, 0:2], train_size=0.7, test_size=0.3, stratify=DF['층'])

stratify 는 분류 문제에서 거의 항상 넣는다. 21회에 “데이터 분할 방법 2가지를 쓰고 적절한 것을 적용, 선택한 이유를 설명”이 나왔는데, 답은 대개 이렇다 — 클래스가 불균형이니 단순 임의 분할이 아니라 층화 분할을 쓰고, 데이터가 적으면 그 위에 교차검증을 얹는다. 시계열이면 둘 다 안 되고 시점 기준 분할이다.

상관과 파생변수

correlation = df.corr()
sns.heatmap(correlation, annot=True, cmap='coolwarm')
plt.show()

sns.pairplot(df)
df.groupby('card3')['isFraud'].mean()          # 그룹별 타깃 평균 — 파생변수 후보를 찾는다
df['income_group'] = pd.cut(df['income'], bins=[0, 2000, 4000, 6000, 8000],
                            labels=['low', 'medium', 'high', 'very high'])

groupby(범주).mean() 으로 타깃 평균을 보는 게 파생변수를 찾는 가장 빠른 방법이다. 그룹 간 차이가 크면 그 범주는 살릴 값어치가 있고, 평평하면 버려도 된다.

sklearn 모듈 지도

어디서 뭘 import 하는지 헷갈릴 때 본다.

모듈하는 일
preprocessing인코딩·정규화·스케일링
impute결측 대치
feature_selection영향이 큰 피처 선택
feature_extraction텍스트·이미지 벡터화 (.text 에 CountVectorizer, TfidfVectorizer)
decomposition차원 축소 (PCA, NMF, TruncatedSVD)
model_selection분할·교차검증·GridSearch
metrics평가 지표 전부
ensembleRandomForest, AdaBoost, GradientBoosting, Voting, Bagging
linear_modelLinearRegression, Ridge, Lasso, LogisticRegression, SGD
naive_bayes · neighbors · svm · tree각 알고리즘
clusterKMeans, 계층형, DBSCAN
pipeline변환과 학습을 묶는다

표시는 이 브라우저에만 남는다. 서버로 가는 것은 없다.