전제 지식 압축 정리 — 데이터 준비와 머신러닝
이 챕터에서 배우는 것
1. 결측치·이상치·인코딩·스케일링을 상황에 맞는 함수로 처리할 수 있다.
2. 데이터 누수 없이 학습/평가(테스트) 데이터를 분리하는 순서를 지킬 수 있다.
3. 머신러닝의 3줄 패턴과 딥러닝의 4단계 패턴의 차이를 설명할 수 있다.시험 연관성: 실기 시험에서 딥러닝 문항이 등장하기 전까지의 앞 단계 전부가 이 챕터의 내용입니다. 앞 단계의 결과물이 그대로 딥러닝 문항의 입력이 되므로, 여기서 어긋나면 뒤 문항이 연쇄적으로 무너집니다.
이 챕터는 딥러닝 본론에 들어가기 위한 전제 지식의 압축 요약입니다. 전처리와 머신러닝을 깊이 파고들지 않고, 시험장에서 손이 먼저 움직여야 하는 패턴만 추려 담았습니다.
2.1 데이터 준비는 다섯 단계로 흐른다
한 줄 요약 — 데이터 준비는 "읽고 → 보고 → 고치고 → 숫자로 바꾸고 → 나눈다"의 다섯 단계이고, 각 단계마다 대표 함수가 하나씩 정해져 있습니다.
시험 문항은 대체로 이 다섯 단계를 따라 순서대로 출제됩니다. 문항이 흩어져 있어 보여도 실제로는 하나의 파이프라인이며, 앞 문항에서 만든 df를 뒤 문항이 그대로 이어받습니다. 그래서 단계별로 변수를 덮어쓸지 새 이름을 줄지 미리 정해두는 습관이 중요합니다.
아래 코드는 이 챕터 전체에서 사용할 예제 데이터를 만듭니다. 시험에서는 read_csv로 파일을 읽지만, 여기서는 재현성을 위해 코드로 생성합니다.
# 이 챕터 예제용 합성 데이터 생성 (시험에서는 이 자리에 read_csv가 들어갑니다)
import numpy as np
import pandas as pd
rng = np.random.default_rng(42)
n = 1000
tenure_months = rng.integers(1, 72, n)
monthly_fee = rng.normal(60, 15, n).round(2)
support_calls = rng.poisson(1.5, n)
contract_type = rng.choice(['month', 'one_year', 'two_year'], n)
# 타깃(churn)이 특성과 관계를 갖도록 확률을 설계
bonus = np.where(contract_type == 'month', 0.8, np.where(contract_type == 'one_year', 0.0, -0.8))
score = -0.04 * tenure_months + 0.02 * monthly_fee + 0.45 * support_calls + bonus
prob = 1 / (1 + np.exp(-(score - score.mean())))
churn = rng.binomial(1, prob)
df = pd.DataFrame({
'tenure_months': tenure_months,
'monthly_fee': monthly_fee,
'support_calls': support_calls,
'contract_type': contract_type,
'churn': churn,
})
# 결측치 5%를 인위적으로 주입
miss_idx = rng.choice(n, size=int(n * 0.05), replace=False)
df.loc[miss_idx, 'monthly_fee'] = np.nan
print(df.shape)
print(df.head())
출력 요지:
(1000, 5)와 상위 5행.monthly_fee에는NaN이 섞여 있습니다.
2.2 먼저 데이터를 본다 — 탐색과 시각화
한 줄 요약 — 전처리를 시작하기 전에 info로 자료형과 결측을, describe로 분포를, value_counts로 범주와 클래스 균형을 확인합니다.
탐색 단계에서 확인해야 할 것은 네 가지로 압축됩니다. ① 행·열의 개수, ② 컬럼별 자료형(숫자인가 문자인가), ③ 결측치가 있는 컬럼과 그 개수, ④ 타깃 클래스의 분포입니다. 특히 ④는 뒤에서 stratify 인자를 쓸지, 평가지표로 정확도만 볼지 말지를 결정하는 근거가 됩니다.
# 탐색 4종 세트 — 앞 절에서 만든 df를 이어서 사용합니다
print(df.info()) # 자료형과 결측 여부
print(df.describe()) # 수치형 분포 요약
print(df.isnull().sum()) # 컬럼별 결측 개수
print(df['churn'].value_counts()) # 타깃 클래스 분포
출력 요지:
monthly_fee의 non-null 개수가 950으로 다른 컬럼보다 적고,isnull().sum()에서monthly_fee 50이 확인됩니다.churn은 0과 1이 대략 5:5 근처로 나옵니다.
시각화는 네 가지만 손에 익혀두면 대응됩니다. 범주형 분포는 countplot, 수치형 분포는 histplot, 수치형 간 상관관계는 heatmap, 그룹별 분포 비교는 boxplot입니다. 앞의 세 가지를 먼저 보겠습니다.
# 시험에 나오는 시각화 기본 3종
import matplotlib.pyplot as plt
import seaborn as sns
sns.countplot(data=df, x='contract_type', hue='churn')
plt.show()
sns.histplot(data=df, x='monthly_fee', bins=30)
plt.show()
num_cols = ['tenure_months', 'monthly_fee', 'support_calls', 'churn']
sns.heatmap(df[num_cols].corr(), annot=True, cmap='Blues')
plt.show()
출력 요지: 그래프 3개.
heatmap의annot=True는 각 칸에 상관계수 숫자를 찍어주는 인자로, 문항에서 자주 지정됩니다.⚠️ 시험 포인트 —
heatmap은 문자열 컬럼이 섞여 있으면 오류가 납니다.df.corr()를 부르기 전에 수치형 컬럼만 골라내거나df.select_dtypes(include='number').corr()를 사용하십시오.
네 번째는 boxplot입니다. 상자 하나가 한 그룹의 분포를 요약해 보여줍니다. 상자의 아래·위 변은 1사분위수와 3사분위수, 상자 안의 선은 중앙값이고, 상자 밖으로 뻗은 수염 끝은 대략적인 분포의 범위입니다. x에 범주형(또는 클래스) 컬럼을, y에 수치형 컬럼을 넣으면 그룹별로 분포가 어떻게 다른지를 한 화면에서 비교할 수 있습니다. "클래스별 센서 값 분포를 비교하시오" 같은 지시에 바로 대응되는 그래프입니다.
# 클래스(churn)별 수치형 분포 비교 — 앞 블록의 df와 sns를 이어서 사용합니다
sns.boxplot(data=df, x='churn', y='monthly_fee')
plt.show()
출력 요지:
churn이 0인 그룹과 1인 그룹의 상자가 나란히 그려집니다. 두 상자의 높이 차이가 크면 그 컬럼이 타깃을 구분하는 데 도움이 되는 특성이라는 신호입니다. 이 예제 데이터에서는 두 상자가 거의 겹치는데, 그것 역시monthly_fee하나만으로는 이탈 여부를 가려내기 어렵다는 정보이므로 그래프를 잘못 그린 것이 아닙니다.
수염 밖으로 벗어난 값은 개별 점으로 찍히는데, 이것이 바로 다음 절에서 IQR 기준으로 걸러낼 이상치입니다. 즉 boxplot은 그룹 비교와 이상치 확인을 동시에 해주는 그래프입니다.
시각화 문항의 출제 비중은 회차에 따라 달라질 수 있으므로, 응시 전 시험 공지 기준을 반드시 확인하십시오.
2.3 결측치 — 지울 것인가 채울 것인가
한 줄 요약 — 결측이 압도적으로 많으면 컬럼을 버리고, 그렇지 않으면 수치형은 평균·중앙값으로, 범주형은 최빈값으로 채웁니다.
실기 시험에서는 대부분 문항이 방법을 직접 지정합니다. "monthly_fee의 결측치를 중앙값으로 대체하시오"처럼 지시가 명확하므로, 스스로 판단하기보다 지시된 함수를 정확히 쓰는 것이 점수에 직결됩니다. 위 분기도는 지시가 없을 때의 기본 판단 기준으로 기억해 두십시오.
평균과 중앙값 중에서는, 분포가 한쪽으로 치우쳤거나 이상치가 있을 때 중앙값(median) 이 안전합니다. 이상치가 평균을 끌어당기기 때문입니다.
# 결측 처리 3종 — 상황에 맞는 것 하나만 쓰면 됩니다
df_drop = df.dropna() # 행 삭제
df_mean = df.copy()
df_mean['monthly_fee'] = df_mean['monthly_fee'].fillna(df_mean['monthly_fee'].mean()) # 평균 대체
df['monthly_fee'] = df['monthly_fee'].fillna(df['monthly_fee'].median()) # 중앙값 대체(이 챕터 기준)
print(df.isnull().sum().sum())
출력 요지:
0. 남은 결측치가 없다는 뜻입니다.
범주형 컬럼은 최빈값으로 채웁니다. mode()는 최빈값이 여러 개일 수 있어 Series를 반환하므로 [0]으로 첫 값을 꺼내야 합니다.
# 범주형 결측치는 최빈값으로 (mode()는 Series를 반환하므로 [0] 필요)
df['contract_type'] = df['contract_type'].fillna(df['contract_type'].mode()[0])
⚠️ 시험 포인트 —
df['col'].fillna(...)는 원본을 바꾸지 않고 새 Series를 반환합니다.df['col'] = df['col'].fillna(...)처럼 반드시 다시 대입하십시오. 결측 처리 코드를 썼는데 다음 문항에서 여전히NaN이 나오는 사고는 거의 이 대입 누락 때문입니다.
2.4 이상치 — IQR로 찾고, 지시대로 처리한다
한 줄 요약 — 사분위수 범위(IQR)를 기준으로 Q1 - 1.5*IQR 미만, Q3 + 1.5*IQR 초과를 이상치로 보고, 삭제하거나 경계값으로 잘라냅니다.
# IQR 기준으로 이상치를 찾고 경계값으로 자르기(clip)
q1 = df['monthly_fee'].quantile(0.25)
q3 = df['monthly_fee'].quantile(0.75)
iqr = q3 - q1
lower, upper = q1 - 1.5 * iqr, q3 + 1.5 * iqr
outliers = df[(df['monthly_fee'] < lower) | (df['monthly_fee'] > upper)]
print('이상치 개수:', len(outliers))
df['monthly_fee'] = df['monthly_fee'].clip(lower, upper) # 경계값으로 대체
# df = df[(df['monthly_fee'] >= lower) & (df['monthly_fee'] <= upper)] # 행 삭제 방식
출력 요지: 이상치 개수가 정수로 출력됩니다(이 예제 데이터에서는 17개).
clip이후에는 최댓값·최솟값이 경계 안으로 들어옵니다.
이상치 처리 방식도 문항이 지정하는 경우가 대부분입니다. "삭제하시오"인지 "경계값으로 대체하시오"인지를 먼저 읽고 그대로 따르십시오.
2.5 인코딩 — 문자열을 숫자로 바꾸는 두 가지 길
한 줄 요약 — 순서가 없는 범주에 라벨 인코딩을 쓰면 모델이 "2가 1보다 크다"고 오해하므로, 원-핫 인코딩(pd.get_dummies)이 기본입니다.
모델은 문자열을 입력받지 못합니다. contract_type 같은 문자 컬럼은 반드시 숫자로 바꿔야 합니다. 방법은 두 가지입니다.
- 라벨 인코딩:
month → 0,one_year → 1,two_year → 2처럼 번호를 붙입니다. 컬럼 수가 늘지 않지만 없던 크기 관계가 생깁니다. - 원-핫 인코딩: 범주 개수만큼 0/1 컬럼을 만듭니다. 컬럼 수는 늘지만 크기 오해가 없습니다.
순서 개념이 있는 범주(예: 저/중/고)라면 라벨 인코딩이 자연스럽고, 순서가 없는 범주라면 원-핫 인코딩이 맞습니다. AICE 실기에서는 원-핫 인코딩, 즉 pd.get_dummies가 압도적으로 자주 요구됩니다.
# 원-핫 인코딩 — columns로 대상 컬럼을 명시하고, dtype=int로 0/1 정수를 만든다
df_enc = pd.get_dummies(df, columns=['contract_type'], dtype=int)
print(df_enc.columns.tolist())
print(df_enc.head(3))
출력 요지:
contract_type컬럼이 사라지고contract_type_month,contract_type_one_year,contract_type_two_year세 컬럼이 0/1 값으로 생깁니다.⚠️ 시험 포인트 — pandas 2.x의
get_dummies는 기본적으로 불리언(True/False) 을 만듭니다.dtype=int를 지정하면 0/1 정수가 되어 이후 스케일링·딥러닝 입력에서 헷갈릴 일이 없습니다. 또한columns=[...]를 생략하면 문자 컬럼 전체가 한꺼번에 변환되므로, 대상 컬럼을 명시하는 습관을 들이십시오.
인코딩은 학습/평가 데이터를 분리하기 전에 전체 데이터프레임에 적용하는 것이 시험장 기준입니다. 분리 후 각각 get_dummies를 하면 한쪽에만 있는 범주 때문에 컬럼 구성이 달라져 모델 입력 shape이 어긋납니다. 뒤에서 볼 스케일링과는 순서 규칙이 반대이므로 구분해서 기억하십시오.
2.6 스케일링 — 특성의 크기를 맞춘다
한 줄 요약 — 값의 범위가 제각각인 특성을 그대로 넣으면 큰 값을 가진 특성이 학습을 지배하므로, 범위를 통일해 줍니다.
tenure_months는 1~72, monthly_fee는 20~100 같은 식으로 특성마다 눈금이 다릅니다. 거리나 가중합을 계산하는 모델은 숫자가 큰 특성에 더 크게 반응하게 됩니다. 딥러닝에서는 스케일링이 사실상 필수입니다. 스케일이 맞지 않으면 학습이 느려지거나 손실 함수가 발산하기도 합니다.
| 스케일러 | 변환 결과 | 언제 쓰나 |
|---|---|---|
StandardScaler |
평균 0, 표준편차 1 | 기본 선택. 분포가 대체로 정규형일 때 |
MinMaxScaler |
최소 0, 최대 1 | 값의 범위를 0~1로 고정하고 싶을 때 |
문항이 특정 스케일러를 지정하면 그것을 씁니다. 지정이 없으면 StandardScaler가 무난합니다.
참고로 트리 계열 머신러닝 모델(예: RandomForestClassifier)은 분기 기준으로 값의 순서만 보기 때문에 스케일링이 성능에 거의 영향을 주지 않습니다. 그래도 파이프라인을 하나로 통일해 두면 딥러닝 문항으로 넘어갈 때 다시 손댈 일이 없습니다.
2.7 분리와 데이터 누수 — 순서가 곧 점수다
한 줄 요약 — 먼저 나누고, 학습 데이터에만 fit_transform, 평가 데이터에는 transform만 합니다.
StandardScaler가 하는 fit은 "평균과 표준편차를 계산해 기억하는" 동작입니다. 전체 데이터로 fit을 하면 평가 데이터의 평균·표준편차 정보가 변환 규칙에 섞여 들어갑니다. 이것을 데이터 누수(data leakage) 라고 합니다. 평가 점수가 실제보다 좋게 나오고, 모델이 처음 보는 데이터에서는 그 성능이 재현되지 않습니다.
순서는 다음 세 줄로 고정하십시오.
train_test_split으로 먼저 나눈다- 학습 데이터에
fit_transform - 평가 데이터에
transform
# 분리 → 학습 데이터에만 fit → 평가 데이터는 transform만
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
X = df_enc.drop(columns=['churn'])
y = df_enc['churn']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train) # 학습 데이터: 계산 + 변환
X_test_scaled = scaler.transform(X_test) # 평가 데이터: 변환만
print(X_train.shape, X_test.shape)
print(y_train.value_counts(normalize=True).round(3))
출력 요지:
(700, 6) (300, 6). 그리고stratify덕분에 학습 데이터의 클래스 비율이 원본 비율과 거의 같게 유지됩니다.
train_test_split의 인자 중 시험에서 반복해서 등장하는 것은 세 가지입니다.
test_size=0.3— 문항이 "7:3으로 나누시오"라고 하면 0.3입니다. 8:2면 0.2입니다.random_state=42— 지정된 숫자를 그대로 씁니다. 채점자가 결과를 재현할 수 있게 하는 장치입니다.stratify=y— 타깃 클래스 비율을 학습/평가 양쪽에 동일하게 유지합니다. 클래스가 불균형할 때 특히 중요합니다.
⚠️ 시험 포인트 — 전체 데이터에
fit_transform을 한 뒤 나누는 것은 대표적인 감점 포인트입니다.scaler.fit_transform(X)한 줄로 끝내고 나서train_test_split을 부르는 코드를 썼다면 순서를 다시 점검하십시오.⚠️ 시험 포인트 — 분류 문제인데
stratify=y를 빠뜨리면, 클래스가 불균형한 데이터에서 평가 데이터에 특정 클래스가 거의 안 들어가는 일이 생깁니다. "층화 추출", "클래스 비율을 유지하여"라는 표현이 문항에 있으면stratify=y를 의미합니다.
2.8 머신러닝 3줄 패턴과 딥러닝 4단계 패턴
한 줄 요약 — sklearn 모델은 "생성 → fit → predict"의 세 줄이면 끝나고, 딥러닝은 그 사이에 "층을 쌓는 구성"과 "손실 함수를 정하는 컴파일"이 추가되어 네 단계가 됩니다.
sklearn 모델은 어떤 알고리즘이든 사용법이 같습니다. 클래스 이름만 바꾸면 되므로, 베이스라인 문항은 다음 세 줄로 대응할 수 있습니다.
# 머신러닝 3줄 패턴 — 베이스라인 성능 확인용
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report
model = RandomForestClassifier(n_estimators=100, random_state=42) # 1) 생성
model.fit(X_train_scaled, y_train) # 2) 학습
y_pred = model.predict(X_test_scaled) # 3) 예측
print(classification_report(y_test, y_pred))
출력 요지: 클래스 0/1 각각의 precision·recall·f1-score와 전체 accuracy가 담긴 표가 출력됩니다.
분류에는 RandomForestClassifier·LogisticRegression, 회귀에는 RandomForestRegressor·LinearRegression을 기억해 두면 베이스라인 문항은 충분히 대응됩니다. 베이스라인의 목적은 최고 성능이 아니라 기준선입니다. 뒤에 만들 딥러닝 모델이 이 점수보다 터무니없이 낮다면 전처리나 모델 설정에 문제가 있다는 신호로 읽으면 됩니다.
딥러닝은 여기에 두 가지가 더 붙습니다. 모델이 "무엇으로 이루어져 있는지"를 직접 쌓아야 하고(구성), "오차를 어떻게 재고 어떻게 줄일지"를 지정해야 합니다(컴파일).
| 단계 | 머신러닝(sklearn) | 딥러닝(Keras) | 담당 챕터 |
|---|---|---|---|
| 1 | 모델 클래스 생성 (한 줄) | 구성 — 층을 쌓는다 | Ch04 |
| 2 | — | 컴파일 — optimizer·손실 함수·metrics 지정 | Ch05 |
| 3 | fit |
학습 — fit(epochs, batch_size, 검증 데이터) |
Ch05 |
| 4 | predict |
평가·예측 — evaluate, predict |
Ch07 |
단계가 늘어난 만큼 조절할 수 있는 것도 많아집니다. 층 수와 노드 수, 활성화 함수(activation function), 손실 함수, epoch 수는 모두 사람이 정하는 값이고, 이 선택 규칙을 익히는 것이 Ch03부터 이어질 본론입니다.
딥러닝 코드는 Ch04부터 실제로 작성합니다. 이 챕터에서는 "sklearn보다 단계가 두 개 많다"는 구조 차이만 잡고 넘어가면 충분합니다.
머신러닝 문항의 포함 여부와 비중은 회차에 따라 달라질 수 있으므로, 시험 공지 기준을 반드시 확인하십시오. 이 교재는 베이스라인 1문항 수준으로 대비합니다.
2.9 평가지표 지도 — 무엇을 볼지 먼저 고른다
한 줄 요약 — 타깃이 범주면 분류 지표(accuracy·precision·recall·f1·혼동 행렬), 숫자면 회귀 지표(mse·mae·r2)를 봅니다.
지표를 고르는 기준은 단순합니다. 타깃 컬럼의 성질이 전부입니다. churn처럼 0/1이면 분류, 집값·잔여 수명처럼 연속된 숫자면 회귀입니다. 문제 유형이 정해지면 출력층 설계와 손실 함수, 평가지표가 한 묶음으로 따라옵니다.
| 문제 유형 | 대표 지표 | sklearn 함수 |
|---|---|---|
| 분류(이진·다중) | accuracy, precision, recall, f1 | classification_report, accuracy_score |
| 분류 — 예측 분포 확인 | 혼동 행렬 | confusion_matrix |
| 회귀 | mse, mae, r2 | mean_squared_error, mean_absolute_error, r2_score |
한 가지만 미리 짚어둡니다. 클래스가 불균형한 데이터(예: 이탈 고객이 10%)에서는 전부 "이탈 아님"이라고만 찍어도 정확도가 90%가 나옵니다. 정확도만으로 판단하지 않는 습관이 필요하며, 그래서 classification_report로 클래스별 precision과 recall을 함께 보는 것입니다.
각 지표를 실제로 계산하고 해석하는 방법은 Ch07에서 다룹니다. 이 챕터에서는 "어떤 상황에 어떤 지표를 고르는가"라는 지도만 머리에 넣어 두십시오.
시험장 표준 코드
데이터 생성부터 베이스라인 성능 출력까지, 이 챕터 전체를 하나로 이은 코드입니다. 시험장에서는 첫 블록의 데이터 생성 부분만 pd.read_csv('data.csv')로 바꾸면 그대로 쓸 수 있습니다.
# ===== 0) import =====
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, confusion_matrix
# ===== 1) 로딩 (시험에서는 df = pd.read_csv('data.csv')) =====
rng = np.random.default_rng(42)
n = 1000
tenure_months = rng.integers(1, 72, n)
monthly_fee = rng.normal(60, 15, n).round(2)
support_calls = rng.poisson(1.5, n)
contract_type = rng.choice(['month', 'one_year', 'two_year'], n)
bonus = np.where(contract_type == 'month', 0.8,
np.where(contract_type == 'one_year', 0.0, -0.8))
score = -0.04 * tenure_months + 0.02 * monthly_fee + 0.45 * support_calls + bonus
prob = 1 / (1 + np.exp(-(score - score.mean())))
df = pd.DataFrame({
'tenure_months': tenure_months,
'monthly_fee': monthly_fee,
'support_calls': support_calls,
'contract_type': contract_type,
'churn': rng.binomial(1, prob),
})
df.loc[rng.choice(n, size=50, replace=False), 'monthly_fee'] = np.nan
# ===== 2) 탐색 =====
print(df.shape)
print(df.isnull().sum())
print(df['churn'].value_counts())
# ===== 3) 결측·이상치 처리 =====
df['monthly_fee'] = df['monthly_fee'].fillna(df['monthly_fee'].median())
q1, q3 = df['monthly_fee'].quantile(0.25), df['monthly_fee'].quantile(0.75)
iqr = q3 - q1
df['monthly_fee'] = df['monthly_fee'].clip(q1 - 1.5 * iqr, q3 + 1.5 * iqr)
# ===== 4) 인코딩 (분리 전에 수행) =====
df = pd.get_dummies(df, columns=['contract_type'], dtype=int)
# ===== 5) 분리 =====
X = df.drop(columns=['churn'])
y = df['churn']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y)
# ===== 6) 스케일링 (분리 후, train에만 fit) =====
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# ===== 7) 머신러닝 베이스라인 =====
model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train_scaled, y_train)
y_pred = model.predict(X_test_scaled)
print(classification_report(y_test, y_pred))
print(confusion_matrix(y_test, y_pred))
출력 요지:
(1000, 5)→ 결측 50건 확인 → 클래스 분포 출력 → 마지막에classification_report표와 2×2 혼동 행렬이 출력됩니다. 이X_train_scaled,X_test_scaled,y_train,y_test가 Ch04 이후 딥러닝 모델의 입력으로 그대로 이어집니다.⚠️ 시험 포인트 — 문항이 지정한 변수명을 그대로 쓰십시오. 뒤 문항이 앞 문항의 변수를 이어받는 구조이므로,
X_train을x_tr처럼 임의로 바꾸면 이어지는 문항의 예시 코드와 어긋납니다.random_state값도 지정된 숫자를 그대로 사용해야 채점 결과가 재현됩니다.
핵심 요약
| 단계 | 핵심 규칙 | 대표 코드 |
|---|---|---|
| 탐색 | 형태·결측·클래스 분포를 먼저 확인 | info(), isnull().sum(), value_counts() |
| 결측치 | 수치형은 평균·중앙값, 범주형은 최빈값. 결과를 다시 대입 | df['c'] = df['c'].fillna(df['c'].median()) |
| 이상치 | IQR 1.5배 밖을 삭제 또는 경계값으로 대체 | df['c'].clip(lower, upper) |
| 인코딩 | 순서 없는 범주는 원-핫. 분리 전에 수행 | pd.get_dummies(df, columns=['c'], dtype=int) |
| 분리 | test_size·random_state·stratify 3종 세트 |
train_test_split(X, y, test_size=0.3, random_state=42, stratify=y) |
| 스케일링 | 분리 후, train만 fit_transform, test는 transform |
scaler.fit_transform(X_train) / scaler.transform(X_test) |
| 모델링 | 머신러닝 3줄(생성·fit·predict), 딥러닝 4단계(구성·컴파일·학습·평가) |
model.fit(...) → model.predict(...) |
| 지표 | 타깃이 범주면 분류 지표, 숫자면 회귀 지표 | classification_report, r2_score |
순서 규칙 한 줄 정리: 인코딩은 분리 전, 스케일링은 분리 후.
Ch02 확인 문제 — 전제 지식 압축 정리(데이터 준비와 머신러닝)
본 문제는 공개된 출제 기준에 따른 유형 연습 문제입니다. 기출 원문이 아닌 기출 변형 문항입니다.
구성: 객관식 2문항 / 코드 빈칸 2문항 / 오류 찾기 2문항
권장 소요 시간: 20분
공통 데이터
Q3~Q6은 아래 데이터를 사용합니다. 본문 2.1의 예제 데이터에 범주형 결측치 20건을 추가로 넣은 것입니다. 실제 시험에서는 이 자리에 df = pd.read_csv('data.csv')가 들어갑니다.
import numpy as np
import pandas as pd
rng = np.random.default_rng(42)
n = 1000
tenure_months = rng.integers(1, 72, n)
monthly_fee = rng.normal(60, 15, n).round(2)
support_calls = rng.poisson(1.5, n)
contract_type = rng.choice(['month', 'one_year', 'two_year'], n)
bonus = np.where(contract_type == 'month', 0.8,
np.where(contract_type == 'one_year', 0.0, -0.8))
score = -0.04 * tenure_months + 0.02 * monthly_fee + 0.45 * support_calls + bonus
prob = 1 / (1 + np.exp(-(score - score.mean())))
df = pd.DataFrame({
'tenure_months': tenure_months,
'monthly_fee': monthly_fee,
'support_calls': support_calls,
'contract_type': contract_type,
'churn': rng.binomial(1, prob),
})
df.loc[rng.choice(n, size=50, replace=False), 'monthly_fee'] = np.nan # 수치형 결측 50건
df.loc[rng.choice(n, size=20, replace=False), 'contract_type'] = np.nan # 범주형 결측 20건
print(df.shape)
print(df.isnull().sum())
출력 요지:
(1000, 5),monthly_fee 50,contract_type 20.각 문항은 위 코드로
df를 새로 만든 직후 상태에서 시작한다고 보십시오. 앞 문항에서 처리한 결과를 이어받지 않습니다(실기 시험의 의존 구조 연습은 모의고사에서 다룹니다).
문제
Q1. (객관식 · 기본) 데이터 준비 순서
범주형 컬럼이 포함된 분류용 데이터를 딥러닝 모델에 넣기 위해 준비하려고 합니다. 데이터 누수를 만들지 않는 올바른 순서는?
① 스케일링 → 원-핫 인코딩 → 학습/평가 데이터 분리
② 원-핫 인코딩 → 학습/평가 데이터 분리 → 학습 데이터에 fit_transform, 평가 데이터에 transform
③ 학습/평가 데이터 분리 → 각각 따로 원-핫 인코딩 → 각각 따로 fit_transform
④ 원-핫 인코딩 → 전체 데이터에 fit_transform → 학습/평가 데이터 분리
⑤ 학습/평가 데이터 분리 → 전체 데이터에 fit_transform → 원-핫 인코딩
Q2. (객관식 · 기본) 인코딩 방식 판별
contract_type 컬럼은 'month', 'one_year', 'two_year' 세 값을 갖는 범주형 컬럼입니다. 이 컬럼의 인코딩에 대한 설명으로 옳은 것은?
① 라벨 인코딩을 적용하면 컬럼이 3개로 늘어난다
② 원-핫 인코딩은 범주에 없던 크기 관계를 만들어 낸다
③ 계약 기간의 순서를 모델이 알 필요가 없는 상황이라면 pd.get_dummies가 기본 선택이다
④ pandas 2.x 이상에서 pd.get_dummies는 별도 지정 없이도 0/1 정수 컬럼을 만든다
⑤ pd.get_dummies(df)처럼 columns 인자를 생략해도 지정한 컬럼 하나만 변환된다
Q3. (코드 빈칸 · 기본) 결측치 처리
monthly_fee의 결측치는 중앙값으로, contract_type의 결측치는 최빈값으로 대체하려고 합니다. 빈칸 (가)~(다)를 채우십시오.
df['monthly_fee'] = df['monthly_fee'].____(가)____(df['monthly_fee'].____(나)____())
df['contract_type'] = df['contract_type'].fillna(df['contract_type'].____(다)____[0])
print(df.isnull().sum().sum())
기대 출력:
0
Q4. (코드 빈칸 · 응용) 분리와 스케일링
공통 데이터의 df를 다음 지시대로 처리하십시오. 결측 처리와 원-핫 인코딩 코드는 이미 채워져 있습니다.
- 학습 데이터와 평가 데이터를 8:2로 분리한다
random_state=42를 사용한다- 타깃
churn의 클래스 비율을 학습/평가 양쪽에 동일하게 유지한다 - 모든 특성을 0~1 범위로 변환한다 (데이터 누수 없이)
빈칸 (가)~(마)를 채우십시오.
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
# --- 앞 단계(제시 코드): 결측 처리 → 원-핫 인코딩 ---
df['monthly_fee'] = df['monthly_fee'].fillna(df['monthly_fee'].median())
df['contract_type'] = df['contract_type'].fillna(df['contract_type'].mode()[0])
df_enc = pd.get_dummies(df, columns=['contract_type'], dtype=int)
X = df_enc.drop(columns=['churn'])
y = df_enc['churn']
# --- 여기부터 빈칸 ---
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=____(가)____, random_state=____(나)____, ____(다)____=y)
scaler = MinMaxScaler()
X_train_scaled = scaler.____(라)____(X_train)
X_test_scaled = scaler.____(마)____(X_test)
print(X_train.shape, X_test.shape)
print('NaN:', np.isnan(X_train_scaled).sum(), np.isnan(X_test_scaled).sum())
기대 출력:
(800, 6) (200, 6)/NaN: 0 0
Q5. (오류 찾기 · 기본) 잘못된 곳 1곳
다음 코드는 오류 메시지 없이 (700, 6) (300, 6)을 출력하며 끝까지 실행됩니다. 그러나 채점에서 감점되는 잘못된 처리가 한 곳 있습니다. 어디이며 왜 문제인지 밝히고, 올바른 코드로 고치십시오.
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
# --- 앞 단계(제시 코드): 결측 처리 → 원-핫 인코딩 ---
df['monthly_fee'] = df['monthly_fee'].fillna(df['monthly_fee'].median())
df['contract_type'] = df['contract_type'].fillna(df['contract_type'].mode()[0])
df_enc = pd.get_dummies(df, columns=['contract_type'], dtype=int)
X = df_enc.drop(columns=['churn'])
y = df_enc['churn']
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
X_train, X_test, y_train, y_test = train_test_split(
X_scaled, y, test_size=0.3, random_state=42, stratify=y)
print(X_train.shape, X_test.shape)
Q6. (오류 찾기 · 응용) 잘못된 곳 2곳
다음 코드도 오류 없이 끝까지 실행됩니다. 그러나 (1) 결측치가 그대로 남고, (2) 클래스가 불균형한 데이터에서 평가 데이터의 클래스 비율이 원본과 어긋납니다. 두 곳을 찾아 고치십시오.
df['monthly_fee'].fillna(df['monthly_fee'].median())
df['contract_type'] = df['contract_type'].fillna(df['contract_type'].mode()[0])
df_enc = pd.get_dummies(df, columns=['contract_type'], dtype=int)
X = df_enc.drop(columns=['churn'])
y = df_enc['churn']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42)
print(df['monthly_fee'].isnull().sum())
해답 및 해설 보기
Q1 해답
정답: ②
정답 코드
# 인코딩은 분리 '전', 스케일링은 분리 '후'
df_enc = pd.get_dummies(df, columns=['contract_type'], dtype=int)
X = df_enc.drop(columns=['churn'])
y = df_enc['churn']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train) # 학습 데이터: 계산 + 변환
X_test_scaled = scaler.transform(X_test) # 평가 데이터: 변환만
왜 이렇게 푸는가
인코딩과 스케일링은 분리 기준으로 순서 규칙이 반대입니다.
- 인코딩은 분리 전 —
get_dummies는 그 데이터에 실제로 존재하는 범주만큼 컬럼을 만듭니다. 분리한 뒤 각각 인코딩하면(③) 한쪽에만 있는 범주 때문에 컬럼 구성이 달라져, 모델 입력 shape이 어긋납니다. - 스케일링은 분리 후 —
fit은 "평균과 표준편차(또는 최솟값·최댓값)를 계산해 기억하는" 동작입니다. 전체 데이터로fit하면(①·④·⑤) 평가 데이터의 통계가 변환 규칙에 섞여 들어갑니다. 이것이 데이터 누수(data leakage) 이고, 평가 점수가 실제보다 좋게 나옵니다.
기억할 한 줄은 이것입니다. 인코딩은 분리 전, 스케일링은 분리 후.
자주 틀리는 지점
scaler.fit_transform(X)한 줄로 끝낸 뒤train_test_split을 부르는 코드(④)가 가장 흔한 감점 패턴입니다. 스케일러 관련 코드를 썼다면fit_transform이X_train에만 걸려 있는지 항상 확인하십시오.- 평가 데이터에도
fit_transform을 쓰는 실수도 잦습니다. 평가 데이터에는transform만 씁니다.
Q2 해답
정답: ③
실증 코드
demo = pd.DataFrame({'contract_type': ['month', 'one_year', 'two_year']})
# 아래 cat.codes는 본문에서 다루지 않는 보조 코드입니다.
# 라벨 인코딩의 '결과 모양'을 눈으로 보이기 위한 용도이며, 시험에서 외울 필요는 없습니다.
label = demo['contract_type'].astype('category').cat.codes
print('라벨 인코딩:', label.tolist(), '/ 컬럼 수 1')
onehot = pd.get_dummies(demo, columns=['contract_type'], dtype=int)
print('원-핫 컬럼:', onehot.columns.tolist(), '/ 컬럼 수', onehot.shape[1])
print('dtype 미지정 시:', pd.get_dummies(demo, columns=['contract_type']).dtypes.iloc[0])
실행 결과: 라벨 인코딩
[0, 1, 2]/ 컬럼 수 1, 원-핫 컬럼['contract_type_month', 'contract_type_one_year', 'contract_type_two_year']/ 컬럼 수 3,dtype미지정 시bool.
왜 이렇게 푸는가
라벨 인코딩은 범주에 번호를 붙이므로 컬럼 수가 늘지 않는 대신, two_year(2)가 one_year(1)보다 "크다"는 없던 크기 관계를 만듭니다. 원-핫 인코딩은 범주 개수만큼 0/1 컬럼을 만들어 이 오해를 없앱니다. 계약 유형처럼 순서를 모델이 알 필요가 없는 범주에는 원-핫 인코딩, 즉 pd.get_dummies가 기본 선택이며 AICE 실기에서 자주 요구됩니다.
오답 선지를 짚으면 이렇습니다. ①은 라벨과 원-핫의 설명이 뒤바뀌었고, ②는 크기 관계를 만드는 쪽이 라벨 인코딩이므로 반대입니다. ④는 위 실증대로 pandas 2.x 이상에서 get_dummies의 기본 결과가 불리언(True/False) 이라 틀렸습니다. ⑤는 columns를 생략하면 문자 컬럼 전체가 한꺼번에 변환되므로 틀렸습니다.
자주 틀리는 지점
dtype=int를 빠뜨리면 True/False 컬럼이 만들어집니다. 그대로도 학습은 되지만 스케일링·딥러닝 입력 단계에서 자료형을 확인할 때 혼란이 생기므로,dtype=int를 붙이는 습관이 안전합니다.columns=[...]생략은 의도치 않은 컬럼까지 원-핫으로 부풀립니다. 대상 컬럼은 항상 명시하십시오.
Q3 해답
정답 코드
df['monthly_fee'] = df['monthly_fee'].fillna(df['monthly_fee'].median()) # (가) fillna, (나) median
df['contract_type'] = df['contract_type'].fillna(df['contract_type'].mode()[0]) # (다) mode()
print(df.isnull().sum().sum())
실행 결과:
0— 남은 결측치가 없습니다.
왜 이렇게 푸는가
- (가)
fillna— 결측치를 지정한 값으로 채우는 함수입니다.dropna는 행을 통째로 버리므로, 문항이 "대체하시오"라고 지시했다면fillna입니다. - (나)
median— 평균(mean)과 중앙값(median) 중 무엇을 쓸지는 분포가 결정합니다. 이상치가 섞여 있거나 분포가 한쪽으로 치우친 경우 평균은 이상치에 끌려가므로 중앙값이 안전합니다. 다만 실기에서는 대체로 문항이 방법을 직접 지정하므로, 지시된 통계량을 그대로 쓰는 것이 점수에 직결됩니다. - (다)
mode()— 범주형은 최빈값으로 채웁니다.mode()는 최빈값이 여러 개일 수 있어 Series를 반환하므로,[0]으로 첫 값을 꺼내야 스칼라가 됩니다.
자주 틀리는 지점
mode()[0]의[0]을 빠뜨리면 Series를 채움값으로 넘기게 되어 의도한 대로 채워지지 않습니다. 수치형의median()·mean()은 스칼라를 반환하므로[0]이 필요 없다는 점과 짝으로 기억하십시오.df['monthly_fee'].fillna(...)는 원본을 바꾸지 않고 새 Series를 반환합니다. 반드시df['col'] = ...로 다시 대입해야 합니다(Q6 참조).
Q4 해답
정답 코드
# --- 앞 단계(제시 코드): 결측 처리 → 원-핫 인코딩 ---
df['monthly_fee'] = df['monthly_fee'].fillna(df['monthly_fee'].median())
df['contract_type'] = df['contract_type'].fillna(df['contract_type'].mode()[0])
df_enc = pd.get_dummies(df, columns=['contract_type'], dtype=int)
X = df_enc.drop(columns=['churn'])
y = df_enc['churn']
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2, # (가) 8:2 분리
random_state=42, # (나) 지시된 값 그대로
stratify=y) # (다) 클래스 비율 유지
scaler = MinMaxScaler()
X_train_scaled = scaler.fit_transform(X_train) # (라) 학습 데이터: 계산 + 변환
X_test_scaled = scaler.transform(X_test) # (마) 평가 데이터: 변환만
print(X_train.shape, X_test.shape)
print('NaN:', np.isnan(X_train_scaled).sum(), np.isnan(X_test_scaled).sum())
print('train 클래스 비율:', y_train.value_counts(normalize=True).round(3).to_dict())
print('test 클래스 비율:', y_test.value_counts(normalize=True).round(3).to_dict())
print('test 스케일 범위:', round(float(X_test_scaled.min()), 4), '~', round(float(X_test_scaled.max()), 4))
실행 결과:
(800, 6) (200, 6)/NaN: 0 0/ train 클래스 비율{1: 0.519, 0: 0.481}/ test 클래스 비율{1: 0.52, 0: 0.48}/ test 스케일 범위-0.1113 ~ 1.0
왜 이렇게 푸는가
- (가)
test_size=0.2—test_size는 평가 데이터의 비율입니다. "8:2로 나누시오"에서 뒤의 숫자가 평가 몫이므로 0.2이고, "7:3"이면 0.3입니다. 0.8을 넣으면 학습 데이터가 200행이 되어 정반대 결과가 나옵니다. - (나)
random_state=42— 분리 결과를 고정해 채점자가 같은 결과를 재현할 수 있게 하는 장치입니다. 지정된 숫자를 그대로 씁니다. - (다)
stratify=y— 타깃 클래스 비율을 학습/평가 양쪽에 동일하게 유지합니다. 문항에 "층화 추출", "클래스 비율을 유지하여"라는 표현이 있으면 이 인자를 뜻합니다. - (라)·(마)
fit_transform/transform— 0~1 범위 변환이라는 지시가 있으므로MinMaxScaler를 쓰고, 최솟값·최댓값 계산(fit)은 학습 데이터에서만 합니다. 평가 데이터는 그 규칙을 적용받기만 하므로transform입니다.
자주 틀리는 지점
- 평가 데이터에
fit_transform을 쓰면,X_test_scaled의 값이 정확히 0~1에 맞춰지면서 오히려 누수가 생깁니다. 올바른 코드에서는 평가 데이터의 값이 0보다 작거나 1보다 큰 구간으로 나올 수 있으며(검증 실행에서 최솟값-0.1113관측), 이는 오류가 아니라 정상입니다. 이 음수 값을 보고 코드를 "고치려다" 누수를 만드는 사례가 많습니다. - 결측 처리를 건너뛰고 스케일링으로 직행하면 조용히 망가집니다. sklearn 스케일러는
NaN을 오류 없이 통과시키므로,X_train_scaled안에NaN이 그대로 남은 채 shape과 클래스 비율만 정상으로 보입니다. 그 배열을 딥러닝 모델에 넣으면 손실이nan이 되어 학습이 전혀 진행되지 않습니다. 스케일링 직후np.isnan(...).sum()으로 0을 확인하는 한 줄이 값싼 보험입니다. stratify=y에y_train이나X를 넣는 실수가 있습니다. 인자에는 분리 대상 타깃 전체(y)를 넣습니다.- 회귀 문제(연속형 타깃)에는
stratify를 쓰지 않습니다. 클래스가 없기 때문에 오류가 납니다.
Q5 해답
잘못된 곳: 학습/평가 데이터를 분리하기 전에 전체 데이터 X에 scaler.fit_transform(X)을 적용한 부분 — 데이터 누수(data leakage)
정답 코드
# --- 앞 단계(제시 코드 그대로): 결측 처리 → 원-핫 인코딩 ---
df['monthly_fee'] = df['monthly_fee'].fillna(df['monthly_fee'].median())
df['contract_type'] = df['contract_type'].fillna(df['contract_type'].mode()[0])
df_enc = pd.get_dummies(df, columns=['contract_type'], dtype=int)
X = df_enc.drop(columns=['churn'])
y = df_enc['churn']
# 1) 먼저 나눈다 ← 스케일링보다 앞으로 옮긴 것이 수정의 핵심
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y)
# 2) 학습 데이터에만 fit_transform
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
# 3) 평가 데이터는 transform만
X_test_scaled = scaler.transform(X_test)
print(X_train_scaled.shape, X_test_scaled.shape)
print(pd.Series(scaler.mean_, index=X_train.columns).round(4).to_string())
실행 결과:
(700, 6) (300, 6)
왜 이렇게 푸는가
StandardScaler의 fit은 각 컬럼의 평균과 표준편차를 계산해 기억하는 동작입니다. 전체 데이터로 fit하면 평가 데이터의 평균·표준편차가 변환 규칙 안으로 들어옵니다. 모델이 학습 시점에 알아서는 안 될 정보를 간접적으로 받은 셈이고, 그 결과 평가 점수가 실제보다 좋게 나오며 새 데이터에서는 그 성능이 재현되지 않습니다.
검증 실행에서 두 방식이 기억한 평균값(scaler.mean_)이 실제로 달랐습니다. 문항 코드 그대로 실행한 6개 컬럼 전체 결과입니다.
컬럼 전체 데이터로 fit(오답) 학습 데이터로만 fit(정답)
tenure_months 36.2120 35.8129
monthly_fee 59.6398 59.9597
support_calls 1.4820 1.4614
contract_type_month 0.3230 0.3329
contract_type_one_year 0.3520 0.3557
contract_type_two_year 0.3250 0.3114
6개 컬럼 전부에서 기준이 어긋납니다. 오답 코드의 기준값에는 평가 데이터 300행의 정보가 섞여 있고, 정답 코드의 기준값은 학습 데이터 700행만으로 계산된 값입니다.
또 하나의 증거는 변환 결과입니다. 정답 코드에서 평가 데이터의 변환 후 평균은 정확히 0이 아니라 아래처럼 0 근처의 값으로 흩어집니다.
tenure_months 0.0641 / monthly_fee -0.0713 / support_calls 0.0547
month -0.0697 / one_year -0.0259 / two_year 0.0977
평가 데이터가 자기 자신의 통계가 아니라 학습 데이터의 기준을 빌려 쓰고 있다는 뜻이며, 이것이 정상 상태입니다. 반대로 평가 데이터의 평균이 정확히 0이 나온다면 누수를 의심해야 합니다.
자주 틀리는 지점
- 이 코드는 오류 없이 실행되고 shape도 정상이므로 스스로 발견하기 어렵습니다. 스케일러 코드를 쓴 직후
fit_transform의 인자가X_train인지 확인하는 것을 고정 절차로 만드십시오. train_test_split에X_scaled(이미 변환된 배열)를 넘기고 있다는 점도 신호입니다. 분리 함수에는 아직 변환하지 않은X가 들어가는 것이 정상 흐름입니다.- 누수가 있어도 평가 점수는 오히려 좋아 보입니다. 점수가 잘 나왔다는 이유로 순서를 검토하지 않고 넘어가는 것이 가장 위험합니다.
Q6 해답
잘못된 곳 2군데
df['monthly_fee'].fillna(...)— 결과를 다시 대입하지 않아 원본df가 그대로입니다.train_test_split(...)— 분류 문제인데stratify=y가 누락되어 클래스 비율이 유지되지 않습니다.
정답 코드
df['monthly_fee'] = df['monthly_fee'].fillna(df['monthly_fee'].median()) # 수정 1: 재대입
df['contract_type'] = df['contract_type'].fillna(df['contract_type'].mode()[0])
df_enc = pd.get_dummies(df, columns=['contract_type'], dtype=int)
X = df_enc.drop(columns=['churn'])
y = df_enc['churn']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y) # 수정 2: stratify
print(df['monthly_fee'].isnull().sum())
print(y_test.value_counts(normalize=True).round(3).to_dict())
실행 결과: 재대입 누락 코드에서는 결측이
50건 그대로 남았고, 재대입 후에는0이 되었습니다.
왜 이렇게 푸는가
수정 1 — pandas의 fillna는 원본을 바꾸지 않고 채워진 새 Series를 반환합니다. 반환값을 받지 않으면 계산만 하고 버리는 셈입니다. 결측 처리 코드를 분명히 썼는데 다음 문항에서 여전히 NaN이 나오는 사고는 거의 전부 이 대입 누락입니다. 실행 로그로 확인한 대로, 재대입이 없으면 결측 50건이 그대로 남습니다.
수정 2 — stratify=y가 없으면 분리가 무작위로 이루어져 클래스 비율이 흔들립니다. 클래스가 균형에 가까울 때는 티가 잘 안 나지만, 불균형 데이터에서는 확연합니다. 검증에서 소수 클래스 비율 5%인 데이터로 비교한 결과입니다.
원본 소수 클래스 비율 : 0.05
stratify 없이 나눈 test 비율 : 0.04
stratify=y로 나눈 test 비율 : 0.05
평가 데이터에 소수 클래스가 덜 들어가면, 그 데이터로 잰 recall·f1이 실제 성능을 반영하지 못합니다. 뒤에 이어지는 딥러닝 문항의 평가까지 함께 흔들리므로 분리 단계에서 잡아야 합니다.
자주 틀리는 지점
- 두 오류 모두 오류 메시지가 나지 않습니다. 그래서 "코드를 썼으니 됐다"고 넘어가기 쉽습니다. 결측 처리 직후에는
print(df.isnull().sum())로, 분리 직후에는print(y_train.value_counts(normalize=True))로 결과를 눈으로 확인하는 습관이 방어책입니다. df['col'].fillna(..., inplace=True)형태를 떠올릴 수 있으나, 최신 pandas에서는 권장되지 않는 방향입니다. 이 교재는 재대입 방식으로 통일합니다.stratify는 분류에서만 씁니다. 타깃이 연속형인 회귀 문제에 넣으면 오류가 납니다.
수정 이력
| 일자 | 대상 | 사유 (QA 리포트 ID) | 조치 |
|---|---|---|---|
| 2026-07-28 | Q5 문제·해답 | QA-CH02-C1 (CRITICAL) — 공통 데이터 실행 규약상 df_enc 미정의로 NameError 발생, 지문의 "오류 없이 실행된다"가 사실과 반대 |
문제·해답 블록 서두에 결측 처리 2줄 + get_dummies 1줄을 제시 코드로 추가해 자기완결화. 지문에 실제 출력 (700, 6) (300, 6) 명시 |
| 2026-07-28 | Q5 해설 | QA-CH02-H2 (HIGH) — 게재 증적이 3컬럼 조건에서 뽑힌 값이라 문항 코드로 재현 불가 | 수정된 문항 코드로 재실행해 6컬럼 전체 scaler.mean_ 대조표로 교체, 평가 데이터 변환 후 평균 6개 값도 교체 |
| 2026-07-28 | Q4 문제·해답 | QA-CH02-H3 (HIGH) — 지문 "전처리를 마친 df_enc"와 달리 결측이 남아 스케일링 결과에 NaN 50개 잔존 |
코드에 결측 처리 2줄 추가, 지문을 "결측 처리와 인코딩 코드는 이미 채워져 있습니다"로 수정. NaN: 0 0 검증 출력 및 "스케일러는 NaN을 조용히 통과시킨다" 감점 포인트 추가 |
| 2026-07-28 | Q2 해설 | QA-CH02-M3 (MEDIUM) — 본문 미출제 API cat.codes 무설명 사용 |
"본문에서 다루지 않는 보조 코드" 주석 2줄 추가 |
| 2026-07-28 | Q2 해설 | QA-CH02-L3 (LOW) — 비공개 기출에 대한 빈도 단정 | "압도적으로 자주 요구됩니다" → "자주 요구됩니다" |