인지야공/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 는 다중공선성 때문이다. 범주가 개면 더미는 개면 충분하고, 개를
다 넣으면 완전 공선성이 생긴다. 회귀에서는 반드시 넣고, 트리에서는 안 넣어도 된다.
스케일링
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. 기본값 |
| MinMaxScaler | 0~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 이 섞여 있어서다. 은 지만
이다.
클래스 불균형
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 | 평가 지표 전부 |
ensemble | RandomForest, AdaBoost, GradientBoosting, Voting, Bagging |
linear_model | LinearRegression, Ridge, Lasso, LogisticRegression, SGD |
naive_bayes · neighbors · svm · tree | 각 알고리즘 |
cluster | KMeans, 계층형, DBSCAN |
pipeline | 변환과 학습을 묶는다 |