실전 모의고사 1회 — 이진분류: 통신사 고객 이탈 예측
본 문제는 공개된 출제 기준에 따른 유형 연습 문제입니다. 기출 문제 원문이 아니며, 공개된 출제 범위·문항 흐름을 바탕으로 재구성한 기출 변형 문제입니다.
| 항목 | 내용 |
|---|---|
| 문항 수 | 12문항 (Q1 → Q12 연속 구조) |
| 총 배점 | 100점 |
| 권장 소요 시간 | 90분 |
| 문제 유형 | 이진분류 (타깃 churn 0/1) |
| 다루는 범위 | 탐색 → 전처리(결측·인코딩·분리·스케일링) → 머신러닝 베이스라인 → 딥러닝 → 평가 |
⚠️ 배점에 관한 안내
본 모의고사의 문항별 배점은 교재 자체 기준입니다. AICE Associate 실기의 실제 문항 수·배점 체계는 공개되어 있지 않으며 회차별로 달라질 수 있습니다. 응시 전 KT AICE 공식 시험 공지 기준을 반드시 확인하십시오.
시작하기 전에 — 답안 작성 규칙
실제 시험과 동일한 조건을 만들기 위해 아래 세 가지를 지키면서 푸십시오.
- 문항 순서대로, 셀을 나눠서 풉니다. 한 셀에 몰아 쓰면 중간 오류 한 번에 앞 단계 결과까지 잃습니다.
- 아래 변수명을 그대로 사용합니다. 뒤 문항이 앞 문항의 변수를 그대로 이어받는 구조입니다.
| 용도 | 변수명 |
|---|---|
| 원본 데이터프레임 | df |
| 특성 / 타깃 | X, y |
| 분리 결과 | X_train, X_test, y_train, y_test |
| 스케일러와 변환 결과 | scaler, X_train_scaled, X_test_scaled |
| 머신러닝 베이스라인 모델 / 그 예측 | rf_model, rf_pred |
| 딥러닝 모델 / 학습 기록 | model, history |
| 콜백 | es(EarlyStopping), mc(ModelCheckpoint) |
| 예측 확률 / 예측 클래스 | pred, y_pred |
-
"출력하시오"라는 지시에는 반드시
print()를 씁니다. 계산만 하고 화면에 남기지 않으면 채점 대상이 되지 않습니다. -
타깃(
y)은 넘파이 배열로 만들어 두면 편합니다. Kerasfit이 받는 표준 형태이고,np.argmax·비교 연산 등 뒤 문항의 넘파이 계산과 형태가 통일됩니다. 이 회차는 pandas Series 그대로도 정상 동작하지만(2회차는to_numpy()를 지시합니다), 넘파이로 통일하는 습관을 권합니다.
(뒤에서 다룰 "학습 셀이 멈추는" 증상은 import 순서 문제이며 타깃 자료형과 무관합니다.)
데이터 준비 (문제 풀이 전 1회 실행)
실제 시험에서는 CSV 파일이 주어집니다. 이 모의고사는 독자가 로컬에서 그대로 재현할 수 있도록, 아래 코드로 동일한 CSV 파일을 직접 생성합니다. 이 블록은 문제가 아니므로 그대로 복사해 한 번만 실행하십시오.
💡 로컬 환경 주의 — 반드시 먼저 읽으십시오 (시험장에서는 신경 쓸 필요 없습니다)
일부 로컬 환경(특히 macOS +pyarrow가 설치된 pandas 조합)에서는pandas를tensorflow보다 먼저 import 하면, 뒤의 학습 셀(Q11)에서model.fit이 아무 출력도 없이 멈춥니다. 그래서 이 교재의 코드 블록은 데이터 생성 블록과 해답의 공통 import 블록 양쪽 모두import tensorflow as tf를 첫 줄에 두었습니다. 한쪽만 고치면 효과가 없습니다 — 먼저 실행된 셀이 pandas를 이미 적재하기 때문입니다.
이미 데이터 생성 셀을 pandas 먼저 import한 상태로 실행했다면, 커널을 재시작한 뒤 아래 블록부터 다시 실행하십시오.
# ===== [문제 아님] 모의고사용 데이터 파일 생성 — 그대로 1회 실행 =====
import tensorflow as tf # [환경] pandas보다 먼저 import — 위 '로컬 환경 주의' 참고
import numpy as np
import pandas as pd
rng = np.random.default_rng(42)
n = 3000
customer_id = np.array([f'C{i:05d}' for i in range(1, n + 1)])
tenure_months = rng.integers(1, 73, n)
monthly_fee = np.round(rng.normal(65, 18, n).clip(20, 130), 2)
contract_type = rng.choice(['month', 'one_year', 'two_year'], n, p=[0.5, 0.3, 0.2])
internet_service = rng.choice(['dsl', 'fiber', 'no'], n, p=[0.35, 0.45, 0.20])
support_calls = rng.poisson(1.2, n)
total_fee = np.round(tenure_months * monthly_fee * rng.normal(1.0, 0.05, n), 2)
contract_bonus = np.where(contract_type == 'month', 0.9,
np.where(contract_type == 'one_year', 0.0, -0.9))
internet_bonus = np.where(internet_service == 'fiber', 0.5,
np.where(internet_service == 'dsl', 0.0, -0.5))
score = (-0.045 * tenure_months + 0.015 * monthly_fee
+ 0.40 * support_calls + contract_bonus + internet_bonus)
prob = 1 / (1 + np.exp(-((score - score.mean()) - 1.2)))
churn = rng.binomial(1, prob)
df_raw = pd.DataFrame({
'customer_id': customer_id,
'tenure_months': tenure_months,
'monthly_fee': monthly_fee,
'total_fee': total_fee,
'contract_type': contract_type,
'internet_service': internet_service,
'support_calls': support_calls,
'churn': churn,
})
miss_idx = rng.choice(n, size=int(n * 0.05), replace=False)
df_raw.loc[miss_idx, 'total_fee'] = np.nan
df_raw.to_csv('telco_churn.csv', index=False)
print('데이터 파일 생성 완료: telco_churn.csv')
데이터 명세
telco_churn.csv — 통신사 고객 3,000명의 가입 정보와 이탈 여부입니다.
| 컬럼 | 설명 | 자료형 | 비고 |
|---|---|---|---|
customer_id |
고객 식별 번호 | 문자열 | 예측에 쓰지 않는 식별자 |
tenure_months |
가입 기간(개월) | 정수 | 1~72 |
monthly_fee |
월 요금 | 실수 | — |
total_fee |
누적 결제 금액 | 실수 | 결측치 있음 |
contract_type |
계약 유형 | 문자열 | month / one_year / two_year |
internet_service |
인터넷 서비스 종류 | 문자열 | dsl / fiber / no |
support_calls |
고객센터 문의 횟수 | 정수 | — |
churn |
타깃 — 이탈 여부 | 정수 | 0 = 유지, 1 = 이탈 |
문제
Q1. 데이터 로딩과 형태 확인 [5점] — 권장 4분
telco_churn.csv 파일을 읽어 데이터프레임 df에 저장하시오.
이어서 다음 세 가지를 출력하시오.
- 데이터의 행·열 개수
- 상위 5개 행
- 컬럼별 자료형과 결측 여부 요약
Q2. 타깃 분포와 결측치 확인 [5점] — 권장 4분
df에 대해 다음 두 가지를 출력하시오.
- 타깃 컬럼
churn의 클래스별 개수와 비율 - 컬럼별 결측치 개수
출력 결과를 보고, 이 데이터가 클래스 불균형 상태인지 판단할 수 있어야 합니다.
Q3. 시각화 [8점] — 권장 8분
다음 두 개의 그래프를 그리시오. (라이브러리는 seaborn, matplotlib 사용)
- 계약 유형(
contract_type)별 이탈 여부 분포를countplot으로 그리시오.
churn값에 따라 막대가 나뉘도록hue를 지정하시오. - 수치형 컬럼 간 상관계수를
heatmap으로 그리시오.
대상 컬럼은tenure_months,monthly_fee,total_fee,support_calls,churn다섯 개이며,
각 칸에 상관계수 숫자가 표시되도록 하시오.
Q4. 결측치 처리 [7점] — 권장 6분
total_fee 컬럼의 결측치를 해당 컬럼의 중앙값으로 대체하시오.
처리 후 데이터프레임 전체에 남은 결측치 개수를 출력하여 0임을 확인하시오.
이후 모든 문항은 결측치가 처리된
df를 이어받습니다.
Q5. 불필요한 컬럼 제거 [5점] — 권장 3분
customer_id 컬럼은 고객을 구분하기 위한 식별자일 뿐 예측에 도움이 되지 않습니다.
df에서 customer_id 컬럼을 제거하고, 제거 후의 컬럼 목록과 데이터 형태를 출력하시오.
Q6. 범주형 변수 인코딩 [8점] — 권장 8분
문자열로 되어 있는 contract_type과 internet_service 두 컬럼을 원-핫 인코딩하시오.
생성되는 더미 변수는 0과 1의 정수가 되도록 하시오.
인코딩 후의 컬럼 목록과 데이터 형태를 출력하시오.
Q7. 학습 데이터와 평가 데이터 분리 [8점] — 권장 6분
churn을 타깃 y로, 나머지 전체 컬럼을 특성 X로 나누시오.
이어서 학습 데이터와 평가 데이터를 7:3으로 분리하여 X_train, X_test, y_train, y_test에 저장하시오.
random_state=42를 사용하시오.- 타깃의 클래스 비율이 학습·평가 양쪽에 동일하게 유지되도록 하시오.
분리 후 X_train과 X_test의 형태, 그리고 y_train·y_test의 클래스 비율을 출력하시오.
Q8. 스케일링 [8점] — 권장 6분
StandardScaler를 사용해 특성을 표준화하시오.
- 스케일러 객체는
scaler라는 이름으로 만드시오. - 평가 데이터의 정보가 변환 규칙에 섞여 들어가지 않도록 처리하시오.
- 변환 결과는 각각
X_train_scaled,X_test_scaled에 저장하시오.
변환 후 두 배열의 형태와, X_train_scaled의 평균·표준편차를 출력하시오.
Q9. 머신러닝 베이스라인 [10점] — 권장 9분
딥러닝 모델의 성능을 비교할 기준선을 만듭니다.
RandomForestClassifier로 베이스라인 모델을 학습시키고 평가 데이터 성능을 확인하시오.
- 모델 변수명은
rf_model로 하시오. (뒤 문항의 딥러닝 모델model과 겹치지 않게 하기 위함입니다.) n_estimators=100,random_state=42를 사용하시오.X_train_scaled로 학습하고X_test_scaled로 예측하시오.- 평가 데이터에 대한 정확도(accuracy) 와
classification_report를 출력하시오.
Q10. 딥러닝 모델 구성과 컴파일 [12점] — 권장 12분
이 문제 유형에 맞는 딥러닝 모델을 model이라는 이름으로 만드시오.
구성 조건
Sequential모델을 사용하시오.- 입력층은
X_train_scaled의 특성 개수에 맞춰 지정하시오. - 은닉층을 2개 이상 쌓되, 활성화 함수는
relu를 사용하시오. - 은닉층 사이에
Dropout을 포함시키시오. - 출력층은 이진분류에 맞는 노드 수와 활성화 함수로 지정하시오.
- 구성 후
model.summary()로 구조를 출력하시오.
컴파일 조건
- optimizer는
adam을 사용하시오. - 손실 함수는 이진분류에 맞는 것을 선택하시오.
- metrics로
accuracy를 지정하시오.
Q11. 콜백을 적용한 학습 [12점] — 권장 14분
model을 학습시키시오.
학습 조건
X_train_scaled,y_train으로 학습하시오.epochs=100,batch_size=32로 지정하시오.- 학습 데이터의 20%를 검증 데이터로 사용하시오.
- 콜백 두 개를 적용하시오.
es—EarlyStopping:val_loss를 관찰하고,patience=5, 가장 좋았던 시점의 가중치를 복원하도록 설정mc—ModelCheckpoint:val_loss기준으로 가장 좋은 모델만best_model.keras파일에 저장- 학습 결과는
history에 저장하시오.
학습 후
- 실제로 학습이 진행된 epoch 수를 출력하시오.
history를 이용해 학습 곡선을 그리시오. loss와 accuracy를 각각 학습·검증 두 선으로 표시하시오.
Q12. 평가와 성능 해석 [12점] — 권장 10분
학습된 model로 평가 데이터의 성능을 확인하시오.
evaluate로 평가 데이터의 loss와 accuracy를 출력하시오.predict로 예측 확률pred를 구하고, 임계값 0.5를 기준으로 클래스y_pred(0 또는 1)로 변환하시오.classification_report와confusion_matrix를 출력하시오.- 평가 데이터를 전부 0(유지)으로만 예측했을 때의 정확도를 계산해 함께 출력하고,
이 데이터에서 정확도(accuracy)만으로 모델 성능을 판단하면 안 되는 이유를 주석으로 2줄 이내로 서술하시오.
해답 및 해설 보기
아래 코드는 모두 실제 실행으로 검증되었습니다. 검증 환경과 결과 수치는 문서 맨 아래 검증 주석을 참고하십시오.
공통 import
문항을 풀기 전에 아래 import 블록을 한 번 실행해 두면 이후 문항에서 반복하지 않아도 됩니다.
import tensorflow as tf # [환경] pandas보다 먼저 import — 아래 '로컬 환경 주의' 참고
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Input, Dense, Dropout
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint
np.random.seed(42)
tf.random.set_seed(42)
💡 로컬 환경 주의 (시험장에서는 신경 쓸 필요 없습니다)
위 블록의 첫 줄이import tensorflow as tf인 것은 오타가 아닙니다. 일부 로컬 환경(macOS +pyarrow가 설치된 pandas 조합)에서는pandas를tensorflow보다 먼저 import 하면 Q11의model.fit이 아무 출력도 없이 멈춥니다. 데이터 준비 절의 생성 블록과 이 공통 import 블록 양쪽 모두 첫 줄을tensorflow로 맞춰야 하며, 한쪽만 고치면 효과가 없습니다.
학습 셀이 진행되지 않고 멈춘다면 커널을 재시작한 뒤 데이터 생성 블록부터 다시 실행하십시오. 이미 pandas가 먼저 적재된 세션에서는 import 순서를 고쳐도 증상이 남습니다.
Q1 해답 — 데이터 로딩과 형태 확인 [5점]
정답 코드
df = pd.read_csv('telco_churn.csv')
print(df.shape) # 1) 행·열 개수
print(df.head()) # 2) 상위 5개 행
print(df.info()) # 3) 자료형과 결측 여부
실행 결과 요지입니다.
(3000, 8)
customer_id tenure_months ... support_calls churn
0 C00001 7 ... 1 0
...
# Column Non-Null Count Dtype
0 customer_id 3000 non-null str
...
3 total_fee 2850 non-null float64
왜 이렇게 푸는가
shape·head·info세 줄이 탐색의 표준 세트입니다.shape로 규모를,head로 값의 생김새를,info로 자료형과 결측을 한 번에 봅니다.- 특히
info()의 Non-Null Count 열이 중요합니다. 전체 3000행인데total_fee만 2850으로 찍히므로, 이 시점에서 이미 "결측 150건"을 알 수 있습니다. Q4에서 어느 컬럼을 손봐야 하는지가 여기서 결정됩니다. info()의 Dtype 열에서str(또는object)로 표시된 컬럼 —customer_id,contract_type,internet_service— 이 Q5·Q6에서 처리 대상이 됩니다. 탐색 단계의 출력이 전처리 계획서 역할을 합니다.
자주 틀리는 지점
df.info()를print()없이 셀 마지막 줄에 두는 것. 노트북에서는 화면에 보이지만, 셀 중간에 있으면 아무것도 출력되지 않습니다. "출력하시오"라는 지시에는 예외 없이print()를 붙이십시오.- 파일 경로를 임의로 바꾸는 것. 문제지에 명시된 파일명을 그대로 써야 채점자가 재현할 수 있습니다.
Q2 해답 — 타깃 분포와 결측치 확인 [5점]
정답 코드
print(df['churn'].value_counts()) # 클래스별 개수
print(df['churn'].value_counts(normalize=True).round(3)) # 클래스별 비율
print(df.isnull().sum()) # 컬럼별 결측치 개수
실행 결과입니다.
churn
0 2113
1 887
churn
0 0.704
1 0.296
customer_id 0
tenure_months 0
monthly_fee 0
total_fee 150
contract_type 0
internet_service 0
support_calls 0
churn 0
왜 이렇게 푸는가
value_counts()는 개수,value_counts(normalize=True)는 비율을 줍니다. 문항이 "비율"까지 요구했으므로 두 줄 모두 필요합니다.- 결과가 70.4% : 29.6% — 이 숫자가 이 모의고사 전체를 관통하는 조건입니다. 두 가지 결정이 여기서 나옵니다.
1. Q7에서stratify=y를 반드시 넣어야 합니다. 비율이 한쪽으로 기울어 있으므로, 무작위로 나누면 평가 데이터의 클래스 비율이 원본과 어긋납니다.
2. Q12에서 accuracy만 보면 안 됩니다. 전부 0으로만 찍어도 정확도가 70%가 나오는 데이터이기 때문입니다. isnull().sum()은 컬럼별 결측 개수를 한 번에 보여 줍니다. Q1의info()로도 알 수 있지만, "결측치 개수를 출력하시오"라는 지시에 가장 정확히 대응하는 함수입니다.
자주 틀리는 지점
df.isnull().sum().sum()(전체 합계, 스칼라 하나)과df.isnull().sum()(컬럼별 Series)을 혼동하는 것. 어느 컬럼인지를 묻는 문항에는.sum()하나만 붙입니다.- 불균형을 확인해 놓고도 뒤 문항에서
stratify를 빠뜨리는 것. Q2의 출력은 보고 끝내는 정보가 아니라 Q7·Q12로 이어지는 근거입니다.
Q3 해답 — 시각화 [8점]
정답 코드
# 1) 계약 유형별 이탈 분포
sns.countplot(data=df, x='contract_type', hue='churn')
plt.title('contract_type vs churn')
plt.show()
# 2) 수치형 컬럼 상관 heatmap
num_cols = ['tenure_months', 'monthly_fee', 'total_fee', 'support_calls', 'churn']
sns.heatmap(df[num_cols].corr(), annot=True, cmap='Blues')
plt.title('numeric correlation')
plt.show()
그래프에서 읽히는 내용입니다(수치로 확인하면 아래와 같습니다).
계약유형별 이탈률
month 0.390
one_year 0.242
two_year 0.125
왜 이렇게 푸는가
countplot의hue가 이 문항의 채점 포인트입니다.hue='churn'을 지정해야 계약 유형마다 막대가 유지/이탈 두 개로 나뉘어, "월 단위 계약이 이탈률이 높다"는 관계가 눈에 보입니다.hue가 없으면 계약 유형별 고객 수만 나올 뿐 이탈과의 관계는 전혀 드러나지 않습니다.heatmap에annot=True를 넣어야 각 칸에 상관계수 숫자가 찍힙니다. "각 칸에 상관계수 숫자가 표시되도록"이라는 지시가 곧annot=True입니다.- 대상 컬럼을
num_cols로 명시적으로 골라낸 이유가 중요합니다.df.corr()를 그냥 부르면customer_id·contract_type같은 문자열 컬럼 때문에 오류가 납니다. 실제로 실행하면 이렇습니다.
ValueError: could not convert string to float: 'C00001'
대안으로 df.select_dtypes(include='number').corr()를 써도 됩니다.
자주 틀리는 지점
- 문자열 컬럼이 섞인 채
df.corr()호출 — 위ValueError가 나는 가장 흔한 실수입니다. 상관 분석 전에는 수치형만 골라내는 한 줄이 반드시 필요합니다. plt.show()를 빠뜨려 그래프가 화면에 남지 않는 것. 시각화 문항은 그림이 출력된 상태가 답안입니다.- 시각화 문항의 출제 비중은 회차에 따라 달라질 수 있으므로, 시험 공지 기준을 확인하십시오. 다만 그리는 데 시간을 많이 쓰지는 마십시오. 배점 대비 시간 소모가 큰 구간입니다.
Q4 해답 — 결측치 처리 [7점]
정답 코드
df['total_fee'] = df['total_fee'].fillna(df['total_fee'].median())
print('남은 결측치 총합:', df.isnull().sum().sum())
실행 결과입니다. (이 데이터에서 total_fee의 중앙값은 2148.03입니다.)
남은 결측치 총합: 0
왜 이렇게 푸는가
fillna는 원본을 바꾸지 않고 새 Series를 반환합니다. 그래서df['total_fee'] = ...형태로 다시 대입해야 합니다. 이 한 가지가 이 문항 배점의 절반입니다.- 중앙값(
median)을 쓰라고 지시한 이유를 이해해 두십시오.total_fee는가입 개월 × 월 요금으로 만들어져 값의 범위가 매우 넓고 오른쪽으로 길게 늘어진 분포입니다. 이런 분포에서 평균은 큰 값들에 끌려가지만 중앙값은 영향을 덜 받습니다. 다만 시험에서는 문항이 방법을 직접 지정하는 경우가 대부분이므로, 스스로 판단하기보다 지시된 함수를 정확히 쓰는 것이 우선입니다. - 마지막 확인 출력이
0이어야 다음 단계로 넘어갈 수 있습니다. 결측치가 남은 채로 딥러닝 학습에 들어가면 오류 없이loss: nan이 나옵니다(Ch08 오류 유형 ③). 여기서 0을 확인해 두는 것이 뒤의 사고를 막습니다.
자주 틀리는 지점
- 재대입 누락.
df['total_fee'].fillna(df['total_fee'].median())만 쓰고 넘어가는 경우입니다. 실제로 실행해 보면 결측치가 그대로 150건 남아 있습니다.
재대입 없이: 150
재대입 후 : 0
df.fillna(df.median())처럼 데이터프레임 전체에 거는 것. 문자열 컬럼이 섞여 있어 의도치 않은 동작을 하거나 오류가 납니다. 지시된 컬럼 하나만 처리하십시오.- 결측 행을
dropna()로 지워 버리는 것. 3,000행 중 150행이 사라지면 뒤 문항의 출력 형태가 전부 달라집니다. 지시는 "중앙값으로 대체"입니다.
Q5 해답 — 불필요한 컬럼 제거 [5점]
정답 코드
df = df.drop(columns=['customer_id'])
print(df.shape)
print(df.columns.tolist())
실행 결과입니다.
(3000, 7)
['tenure_months', 'monthly_fee', 'total_fee', 'contract_type',
'internet_service', 'support_calls', 'churn']
왜 이렇게 푸는가
drop도 새 데이터프레임을 반환합니다.df = df.drop(...)처럼 다시 대입해야 합니다(Q4와 같은 원리입니다).customer_id를 지우는 이유는 단순히 "쓸모없어서"가 아닙니다. 이 컬럼은 문자열이므로 남겨 두면 Q8의StandardScaler에서 곧바로 오류가 납니다. 실제로 남긴 채 스케일링을 시도하면 이렇습니다.
ValueError: could not convert string to float: 'C00001'
즉 Q5는 독립된 정리 작업이 아니라 Q8이 성립하기 위한 선행 조건입니다.
- 만약 customer_id가 숫자 ID(예: 1, 2, 3, ...)였다면 오류는 안 나지만 더 위험합니다. 모델이 의미 없는 번호를 특성으로 학습하기 때문입니다. 식별자 컬럼은 자료형과 무관하게 제거가 원칙입니다.
자주 틀리는 지점
df.drop(columns=['customer_id'])만 쓰고 재대입하지 않는 것.df.drop('customer_id')처럼 축을 지정하지 않는 것.drop의 기본 축은 행(index) 이라KeyError가 납니다. 컬럼을 지울 때는columns=[...]또는axis=1이 필요합니다.
Q6 해답 — 범주형 변수 인코딩 [8점]
정답 코드
df = pd.get_dummies(df, columns=['contract_type', 'internet_service'], dtype=int)
print(df.shape)
print(df.columns.tolist())
실행 결과입니다.
(3000, 11)
['tenure_months', 'monthly_fee', 'total_fee', 'support_calls', 'churn',
'contract_type_month', 'contract_type_one_year', 'contract_type_two_year',
'internet_service_dsl', 'internet_service_fiber', 'internet_service_no']
왜 이렇게 푸는가
columns=[...]로 대상 컬럼을 명시합니다. 생략하면 문자 컬럼 전체가 한꺼번에 변환되는데, 이 단계에서는 문제가 없어 보여도 다른 데이터에서는 예상 밖의 컬럼까지 더미로 바뀝니다. 명시하는 습관이 안전합니다.dtype=int가 이 문항의 채점 포인트입니다. pandas 2.x 이후get_dummies의 기본 출력은 불리언(True/False) 입니다. 문항이 "0과 1의 정수"를 요구했으므로 반드시 지정해야 합니다.
dtype 미지정: bool
dtype=int : int64
- 컬럼 수 변화를 확인하십시오. 7개 → 11개입니다. 범주형 2개가 사라지고 각각 3개씩 총 6개의 더미 컬럼이 생겼기 때문입니다(7 − 2 + 6 = 11). 이 11개 중
churn을 뺀 10개가 Q10의 입력 특성 수가 됩니다. - 라벨 인코딩이 아니라 원-핫 인코딩을 쓰는 이유는,
month/one_year/two_year에0/1/2를 붙이면 모델이 "two_year가 month의 2배"라는 없던 크기 관계를 학습하기 때문입니다.
자주 틀리는 지점
dtype=int누락. 불리언 컬럼도 스케일링·학습은 통과하지만, 지시를 어긴 것이므로 감점 대상입니다.- 인코딩을 데이터 분리 뒤에 하는 것. 순서 규칙은 인코딩은 분리 전, 스케일링은 분리 후입니다. 분리 후에 각각 인코딩하면 한쪽에만 있는 범주 때문에 컬럼 구성이 달라집니다. 실제로 평가 데이터가 작을 때 재현해 보면 이렇습니다.
test 표본 3개일 때: train 컬럼 10개 / test 컬럼 7개
누락 컬럼 ['contract_type_one_year', 'contract_type_two_year', 'internet_service_dsl']
이 상태로 predict를 부르면 입력 shape 불일치 오류가 납니다.
- churn까지 인코딩 대상에 넣는 것. 타깃은 이미 0/1 정수이므로 건드리지 않습니다.
Q7 해답 — 학습 데이터와 평가 데이터 분리 [8점]
정답 코드
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)
print(X_train.shape, X_test.shape)
print(y_train.value_counts(normalize=True).round(3))
print(y_test.value_counts(normalize=True).round(3))
실행 결과입니다.
(2100, 10) (900, 10)
churn
0 0.704
1 0.296
churn
0 0.704
1 0.296
왜 이렇게 푸는가
test_size=0.3이 "7:3"입니다. 인자가 지정하는 것은 평가 데이터의 비율이라는 점을 기억하십시오. 8:2면0.2입니다.test_size=0.7로 쓰면 정반대가 됩니다.random_state=42는 채점자가 같은 결과를 재현하기 위한 장치입니다. 지시된 숫자를 그대로 씁니다.- "클래스 비율이 양쪽에 동일하게 유지되도록" =
stratify=y입니다. 문항이stratify라는 단어를 직접 쓰지 않고 "층화 추출", "클래스 비율을 유지하여"처럼 우회 표현하는 경우가 많으므로, 이 표현을 인자로 옮기는 연습이 필요합니다. - 효과를 수치로 확인하면 이렇습니다.
stratify없이 나누면random_state값에 따라 평가 데이터의 이탈 비율이 흔들립니다.
stratify 없음 random_state=0 → test 클래스1 비율 0.2878
stratify 없음 random_state=1 → test 클래스1 비율 0.2967
stratify 없음 random_state=7 → test 클래스1 비율 0.3011
stratify=y → test 클래스1 비율 0.2956 (원본 0.2957)
stratify=y를 쓰면 원본 비율 0.2957이 소수 넷째 자리까지 그대로 재현됩니다.
- X는 churn을 제외한 전체입니다. drop(columns=['churn'])으로 만들면 Q6에서 생성된 더미 컬럼까지 자동으로 포함되므로, 컬럼을 하나씩 나열하는 것보다 안전합니다.
자주 틀리는 지점
- 반환 순서 착각.
train_test_split은X_train, X_test, y_train, y_test순서로 반환합니다.X_train, y_train, X_test, y_test로 받으면 오류 없이 진행되다가 학습 단계에서 이해할 수 없는 shape 오류가 납니다. stratify=y누락. 이 데이터처럼 불균형이 있을 때 특히 손해가 큽니다.stratify='churn'처럼 문자열을 넣는 것. 타깃 배열 자체(y)를 넣어야 합니다.
Q8 해답 — 스케일링 [8점]
정답 코드
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train) # 학습 데이터: 계산 + 변환
X_test_scaled = scaler.transform(X_test) # 평가 데이터: 변환만
print(X_train_scaled.shape, X_test_scaled.shape)
print('학습 데이터 평균:', X_train_scaled.mean().round(3),
'/ 표준편차:', X_train_scaled.std().round(3))
실행 결과입니다.
(2100, 10) (900, 10)
학습 데이터 평균: 0.0 / 표준편차: 1.0
왜 이렇게 푸는가
- "평가 데이터의 정보가 변환 규칙에 섞이지 않도록" = 학습 데이터에만
fit하라는 뜻입니다.fit은 "평균과 표준편차를 계산해 기억하는" 동작입니다. 전체 데이터로fit하면 평가 데이터의 통계량이 변환 규칙에 들어가고, 이것이 데이터 누수(data leakage) 입니다. 평가 점수가 실제보다 좋게 나와 모델을 과대평가하게 됩니다. - 그래서 순서는 세 줄로 고정됩니다. ① 먼저 나눈다 → ②
X_train에fit_transform→ ③X_test에transform. - 학습 데이터의 평균이
0.0, 표준편차가1.0으로 나오는 것이StandardScaler가 제대로 동작했다는 증거입니다. 반면 평가 데이터의 평균은 정확히 0이 아닙니다(이 데이터에서는0.011). 학습 데이터 기준으로 변환했기 때문이며, 이것이 정상입니다. - 딥러닝에서 스케일링은 사실상 필수입니다.
total_fee는 수천 단위,support_calls는 한 자릿수인데 이대로 넣으면 큰 값을 가진 특성이 가중치 갱신을 지배해 학습이 제대로 진행되지 않습니다.
자주 틀리는 지점
- 평가 데이터에도
fit_transform을 쓰는 것. 가장 흔한 감점 패턴입니다.X_test_scaled = scaler.fit_transform(X_test)로 쓰면 평가 데이터의 자체 통계로 다시 변환되어 학습 데이터와 기준이 달라집니다. - 분리 전에
scaler.fit_transform(X)한 줄로 끝내고 나서train_test_split을 부르는 것. 오류가 나지 않아 알아채기 어렵습니다. - 뒤 문항에서 스케일링 안 된
X_train을 그대로 넣는 것. 딥러닝 모델에는 반드시X_train_scaled를 넣습니다. 변수명이 비슷해 헷갈리기 쉬우니, 학습 코드를 쓸 때_scaled가 붙어 있는지 눈으로 한 번 확인하십시오.
Q9 해답 — 머신러닝 베이스라인 [10점]
정답 코드
rf_model = RandomForestClassifier(n_estimators=100, random_state=42) # 1) 생성
rf_model.fit(X_train_scaled, y_train) # 2) 학습
rf_pred = rf_model.predict(X_test_scaled) # 3) 예측
print('RandomForest accuracy:', round(accuracy_score(y_test, rf_pred), 4))
print(classification_report(y_test, rf_pred))
실행 결과입니다.
RandomForest accuracy: 0.73
precision recall f1-score support
0 0.78 0.87 0.82 634
1 0.56 0.40 0.47 266
accuracy 0.73 900
macro avg 0.67 0.63 0.64 900
weighted avg 0.71 0.73 0.72 900
왜 이렇게 푸는가
- sklearn 모델은 "생성 →
fit→predict" 세 줄이면 끝납니다. 알고리즘이 바뀌어도 클래스 이름만 교체하면 됩니다. random_state=42가 여기에도 필요합니다. 랜덤 포레스트는 이름 그대로 무작위성을 쓰는 모델이라, 지정하지 않으면 실행할 때마다 성능이 달라져 재현이 되지 않습니다.train_test_split에만 넣고 모델에는 빠뜨리는 경우가 많습니다.- 모델 변수명을
rf_model로 지정한 것이 이 문항의 숨은 함정입니다. 교재 표준에서model은 Keras 모델을 가리킵니다. 여기서model = RandomForestClassifier(...)로 써 두면, Q10에서model = Sequential()을 만드는 순간 베이스라인이 사라집니다. 그 자체로는 오류가 아니지만, "딥러닝이 베이스라인보다 나은가"를 마지막에 다시 비교할 수 없게 됩니다. 실기에서는 앞뒤 문항이 같은 변수를 이어 쓰므로, 이름 충돌을 미리 피하는 습관이 필요합니다. - 베이스라인의 목적은 최고 성능이 아니라 기준선입니다. 뒤에 만들 딥러닝 모델이 이 점수보다 크게 낮다면 전처리나 모델 설정에 문제가 있다는 신호로 읽으면 됩니다.
- 여기서 이미 불균형의 그림자가 보입니다. 전체 accuracy는
0.73으로 나쁘지 않아 보이지만, 정작 찾아내야 할 이탈 고객(클래스 1)의 recall은0.40입니다. 실제 이탈자 266명 중 107명만 잡아냈다는 뜻입니다. Q12에서 이 관점을 다시 씁니다.
자주 틀리는 지점
- 모델에
random_state를 빠뜨리는 것. - 원본
X_train을 넣는 것. 랜덤 포레스트는 스케일링에 둔감해서 성능이 크게 달라지지 않지만, 문항이X_train_scaled를 쓰라고 지시했다면 지시를 따르는 것이 채점 기준입니다. accuracy_score(rf_pred, y_test)처럼 인자 순서를 바꾸는 것. accuracy는 대칭이라 값이 같지만,classification_report는 첫 번째가 실제값, 두 번째가 예측값이라는 순서가 지켜져야 precision·recall이 뒤바뀌지 않습니다.
Q10 해답 — 딥러닝 모델 구성과 컴파일 [12점]
정답 코드
# ===== 구성 =====
model = Sequential()
model.add(Input(shape=(X_train_scaled.shape[1],)))
model.add(Dense(64, activation='relu'))
model.add(Dropout(0.3))
model.add(Dense(32, activation='relu'))
model.add(Dropout(0.3))
model.add(Dense(1, activation='sigmoid')) # 이진분류 출력층
model.summary()
# ===== 컴파일 =====
model.compile(optimizer='adam',
loss='binary_crossentropy', # 이진분류 손실 함수
metrics=['accuracy'])
summary() 출력의 요지입니다.
Layer (type) Output Shape Param #
dense (Dense) (None, 64) 704
dropout (Dropout) (None, 64) 0
dense_1 (Dense) (None, 32) 2,080
dropout_1 (Dropout) (None, 32) 0
dense_2 (Dense) (None, 1) 33
Total params: 2,817
왜 이렇게 푸는가
- 입력 shape를
X_train_scaled.shape[1]로 자동 계산합니다. 이 데이터의 특성은 10개지만, 숫자10을 직접 적으면 앞 문항에서 컬럼이 하나만 달라져도 shape 불일치 오류가 납니다.shape[1]로 쓰면 Q6·Q7의 결과가 무엇이든 자동으로 맞습니다. Q10이 Q6·Q7의 결과를 이어받는 지점이 바로 이 한 줄입니다. - 출력층
Dense(1, activation='sigmoid')인 이유는 이진분류이기 때문입니다. 노드가 1개인 것은 "이탈일 확률" 하나만 있으면 충분하기 때문이고(유지 확률은 1에서 빼면 됩니다),sigmoid인 것은 출력을 0~1 확률로 눌러 주기 위해서입니다. binary_crossentropy와sigmoid는 언제나 짝으로 움직입니다. 손실 함수만 바꾸고 출력층을 그대로 두거나, 그 반대로 하는 것이 가장 흔한 사고입니다.Dropout은 은닉층Dense뒤에만 넣습니다. 출력층 뒤에 두면 최종 확률값 자체가 무작위로 0이 되어 예측이 망가집니다. 비율0.3은 관례적 기본값이며, 문항에 지정이 없으면 0.2~0.5 범위에서 고르면 됩니다.- 은닉층 노드 수를
64 → 32로 줄여 가며 쌓는 것은 관례적인 구성입니다. 성능에 결정적이지는 않으므로 여기에 시간을 쓰지 마십시오. "은닉층 2개 이상", "Dropout 포함"이라는 지시 조건을 충족했는지가 채점 항목입니다.
자주 틀리는 지점
- 입력 shape에 샘플 수를 넣는 것.
Input(shape=(2100, 10))은 틀립니다. 행 수는 넣지 않습니다. - 출력층에
softmax를 쓰는 것. 노드가 1개인 층에softmax를 걸면 그 값은 언제나 1이 되어 학습이 전혀 되지 않습니다. 오류도 나지 않아 발견이 늦습니다. metrics='accuracy'를 리스트 없이 쓰는 것. 최신 버전에서는 통과하는 경우가 있지만,metrics=['accuracy']처럼 리스트로 감싸는 것이 표준입니다.- 구성만 하고
compile()을 빠뜨리는 것. 컴파일 없이fit을 부르면 "모델이 컴파일되지 않았다"는 오류가 납니다. 구성과 컴파일은 한 세트로 붙여 쓰십시오.
Q11 해답 — 콜백을 적용한 학습 [12점]
정답 코드
es = EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True)
mc = ModelCheckpoint('best_model.keras', monitor='val_loss', save_best_only=True)
history = model.fit(X_train_scaled, y_train,
epochs=100, batch_size=32,
validation_split=0.2,
callbacks=[es, mc], verbose=0) # verbose는 0/1 무엇이든 무방
print('실제로 학습한 epoch 수:', len(history.history['loss']))
print('history 키:', list(history.history.keys()))
best_epoch = int(np.argmin(history.history['val_loss'])) + 1
print('최저 val_loss:', round(min(history.history['val_loss']), 4), '@ epoch', best_epoch)
# 학습 곡선
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
plt.plot(history.history['loss'], label='train loss')
plt.plot(history.history['val_loss'], label='val loss')
plt.xlabel('epoch'); plt.ylabel('loss'); plt.legend()
plt.subplot(1, 2, 2)
plt.plot(history.history['accuracy'], label='train accuracy')
plt.plot(history.history['val_accuracy'], label='val accuracy')
plt.xlabel('epoch'); plt.ylabel('accuracy'); plt.legend()
plt.tight_layout()
plt.show()
실행 결과입니다.
실제로 학습한 epoch 수: 10
history 키: ['accuracy', 'loss', 'val_accuracy', 'val_loss']
최저 val_loss: 0.5165 @ epoch 5
왜 이렇게 푸는가
epochs=100을 지정했는데 10에서 끝난 것이EarlyStopping이 동작했다는 증거입니다.val_loss가 5번째 epoch에서 최저를 찍은 뒤 5번(patience=5) 더 지켜봤지만 개선되지 않아 10에서 중단했습니다.print('실제로 학습한 epoch 수:', len(history.history['loss']))한 줄이 그 확인 방법입니다. 중단 시점은 실행마다 달라집니다(검증 환경에서 10~20 범위). 숫자 자체가 아니라 상한 100보다 훨씬 앞에서 끝났다는 사실이 채점 포인트입니다.verbose는 0이든 1이든 상관없습니다. 1이면 epoch마다 진행 로그가 흐르고 0이면 조용합니다. 다만 화면 로그는 채점 근거가 아니므로, 어느 쪽을 쓰든 위의len(history.history['loss'])출력은 남겨야 합니다. (2회차 Q11도 같은 규칙입니다.)restore_best_weights=True가 이 문항의 핵심 인자입니다. 이것을 빠뜨리면 학습은 최저점에서 멈추지만 모델에 남는 가중치는 중단 시점(10번째)의 것입니다. 즉 최고 성능 지점을 지나쳐 놓고 그보다 나쁜 상태로 평가하게 됩니다. "가장 좋았던 시점의 가중치를 복원"이라는 지시가 곧 이 인자입니다.ModelCheckpoint의save_best_only=True는 매 epoch마다 덮어쓰지 않고val_loss가 개선될 때만 파일을 갱신합니다. 확장자는.keras를 쓰십시오. Keras 3에서.h5는 여전히 동작하지만.keras가 표준이며, 확장자 없이'best_model'처럼 쓰면 오류가 납니다.callbacks=[es, mc]처럼 리스트로 전달해야 합니다. 콜백 객체를 만들어 놓고fit에 넘기지 않으면 아무 일도 일어나지 않습니다. 오류도 나지 않아 "EarlyStopping을 넣었는데 100 epoch을 다 돌았다"는 상황이 생깁니다.validation_split=0.2는X_train의 뒤쪽 20%를 잘라 검증에 씁니다. 여기서 잘라내는 것은 학습 데이터의 일부이며,X_test는 이 과정에 전혀 관여하지 않습니다. 평가 데이터는 마지막 Q12에서 딱 한 번만 씁니다.- 학습 곡선은
history.history의 네 개 키로 그립니다. 키 이름이 기억나지 않으면print(history.history.keys())로 먼저 확인하십시오. 이진분류에서는accuracy/val_accuracy지만, 회귀에서는mae/val_mae가 되므로 유형에 따라 달라집니다.
자주 틀리는 지점
restore_best_weights=True누락 — 지시문에 명시된 조건이므로 그 자체로 감점이며, 평가 성능도 손해를 봅니다.callbacks=[es, mc]전달 누락 또는callbacks=es처럼 리스트 없이 넘기는 것.validation_split과validation_data혼동.validation_split은 비율(숫자),validation_data는(X_val, y_val)튜플입니다. 둘을 바꿔 쓰면 오류가 납니다.X_train(스케일링 전)을 넣는 것. 이 경우 오류는 없지만 accuracy가 낮은 값에서 잘 오르지 않습니다.- 학습 곡선에서
history.history['acc']처럼 옛 키 이름을 쓰는 것. 최신 Keras는accuracy입니다.KeyError가 나면 키 목록부터 출력하십시오.
시간 관리 조언 — 시험장에서
epochs=100을 그대로 걸고 기다리다 시간을 잃는 일이 흔합니다. 먼저epochs=20정도로 한 번 완주시켜 Q12의 평가 출력까지 만들어 두고, 시간이 남으면epochs=100으로 다시 돌리십시오.EarlyStopping은 어느 쪽이든 반드시 넣어 둡니다(지시 조건이므로).
Q12 해답 — 평가와 성능 해석 [12점]
정답 코드
# 1) evaluate
loss, acc = model.evaluate(X_test_scaled, y_test, verbose=0)
print('test loss:', round(loss, 4), '/ test accuracy:', round(acc, 4))
# 2) predict → 임계값 0.5로 클래스 변환
pred = model.predict(X_test_scaled, verbose=0)
y_pred = (pred > 0.5).astype(int).flatten()
# 3) 상세 지표
print(classification_report(y_test, y_pred, digits=3))
print(confusion_matrix(y_test, y_pred))
# 4) 전부 0으로만 예측했을 때의 정확도
naive = np.zeros(len(y_test), dtype=int)
print('무조건 0 예측 accuracy:', round(accuracy_score(y_test, naive), 4))
# 이 데이터는 유지:이탈이 약 7:3이라 전부 0으로 찍어도 정확도가 0.70이 나온다.
# 따라서 accuracy가 아니라 클래스 1의 recall과 f1으로 판단해야 한다.
실행 결과입니다.
test loss: 0.4986 / test accuracy: 0.7467
precision recall f1-score support
0 0.801 0.852 0.826 634
1 0.584 0.496 0.537 266
accuracy 0.747 900
macro avg 0.693 0.674 0.681 900
weighted avg 0.737 0.747 0.740 900
[[540 94]
[134 132]]
무조건 0 예측 accuracy: 0.7044
왜 이렇게 푸는가
evaluate와predict는 반환값이 다릅니다.evaluate는 컴파일 때 지정한[loss, accuracy]두 숫자를 돌려주고,predict는 샘플별 확률 배열(900, 1)을 돌려줍니다. 문항이 "loss와 accuracy"를 요구하면evaluate, "예측 결과"를 요구하면predict입니다.(pred > 0.5).astype(int)가 확률 → 클래스 변환입니다.pred에는0.009,0.166같은 확률이 들어 있고, 이대로는 분류 지표를 계산할 수 없습니다. 실제로 확률을 그대로 넣으면 이런 오류가 납니다.
ValueError: Classification metrics can't handle a mix of binary and continuous targets
.flatten()은 (900, 1)을 (900,)으로 펴 주는 정리 작업입니다. sklearn은 (N, 1) 형태도 받아 주지만, y_test와 형태를 맞춰 두면 이후 계산에서 헷갈릴 일이 없습니다.
- 혼동 행렬을 읽는 법입니다. [[540, 94], [134, 132]]는 행이 실제값, 열이 예측값입니다.
| 예측 0 | 예측 1 | |
|---|---|---|
| 실제 0 (유지) | 540 (맞춤) | 94 (헛짚음) |
| 실제 1 (이탈) | 134 (놓침) | 132 (잡아냄) |
즉 실제 이탈 고객 266명 중 134명을 놓쳤습니다.
- 4번이 이 모의고사 전체의 결론입니다. 아무 학습도 하지 않고 전부 "유지"로만 찍어도 정확도가 0.7044입니다. 우리 모델의 정확도는 0.7467 — 겨우 4%p 높습니다. accuracy만 보면 "70% 넘으니 괜찮다"고 오판하게 되는 구조입니다.
- 실제로 봐야 할 것은 클래스 1(이탈)의 recall 0.496과 f1 0.537 입니다. 전부 0으로 찍는 모델은 클래스 1의 recall이 0.000이므로, 이 지표에서 우리 모델이 확실한 개선을 보였다는 것이 드러납니다. classification_report가 필요한 이유가 정확히 여기에 있습니다.
- support 열(634 / 266)도 함께 보십시오. 클래스별 표본 수가 크게 다르면 그 자체가 불균형 신호이고, 이때 macro avg(클래스를 동등 취급)와 weighted avg(표본 수로 가중)의 차이가 벌어집니다. 여기서도 macro f1 0.681 vs weighted f1 0.740으로 벌어져 있습니다.
- 참고로 Q9의 랜덤 포레스트는 accuracy 0.73 / 클래스 1 recall 0.40이었습니다. 딥러닝 모델이 정확도는 비슷하지만 이탈 고객을 더 많이 잡아냈다는 점이 실질적인 개선입니다. 베이스라인을 남겨 둔 덕분에 이 비교가 가능합니다(Q9에서 변수명을 rf_model로 분리한 이유입니다).
자주 틀리는 지점
- 확률값을 그대로
classification_report에 넣는 것. 위ValueError가 나는 대표적인 사고입니다. 임계값 변환 한 줄을 반드시 거치십시오. np.argmax(pred, axis=1)를 쓰는 것. 이것은 다중분류용입니다. 이진분류의pred는 열이 1개뿐이라argmax를 걸면 전부 0이 나옵니다. 오류가 나지 않아 발견이 매우 늦습니다.- accuracy만 출력하고 끝내는 것. 이 문항은 "정확도만으로 판단하면 안 되는 이유"까지 요구합니다. 불균형 데이터에서는
classification_report와 혼동 행렬이 함께 있어야 답안이 완성됩니다. evaluate의 반환값을 하나로 받는 것.acc = model.evaluate(...)로 쓰면acc에 리스트[loss, accuracy]가 통째로 들어갑니다. 컴파일 때 metrics를 몇 개 지정했는지에 따라 반환 개수가 달라진다는 점을 기억하십시오.
채점표
| Q | 단계 | 배점 | 핵심 채점 요소 |
|---|---|---|---|
| 1 | 로딩·형태 | 5 | read_csv / shape·head·info 출력 |
| 2 | 탐색 | 5 | value_counts 개수+비율 / isnull().sum() |
| 3 | 시각화 | 8 | countplot의 hue='churn' / heatmap의 annot=True / 수치형만 선택 |
| 4 | 결측 처리 | 7 | median() 사용 / 재대입 / 결과 확인 출력 |
| 5 | 컬럼 정리 | 5 | drop(columns=[...]) / 재대입 |
| 6 | 인코딩 | 8 | get_dummies / columns 명시 / dtype=int |
| 7 | 분리 | 8 | test_size=0.3 / random_state=42 / stratify=y / 반환 순서 |
| 8 | 스케일링 | 8 | train fit_transform / test transform만 |
| 9 | 머신러닝 | 10 | rf_model 이름 / n_estimators=100·random_state=42 / 성능 출력 |
| 10 | DL 구성·컴파일 | 12 | Input(shape=(n,)) / 은닉층 2개 이상 + Dropout / Dense(1,'sigmoid') / binary_crossentropy |
| 11 | 학습 | 12 | epochs=100·batch_size=32 / validation_split=0.2 / restore_best_weights=True / callbacks=[es, mc] / 학습 곡선 |
| 12 | 평가 | 12 | evaluate / 임계값 0.5 변환 / classification_report·confusion_matrix / 불균형 해석 |
| — | 합계 | 100 | — |
배점은 교재 자체 기준입니다. 실제 시험의 배점 체계는 공개되어 있지 않으므로 학습 우선순위를 정하는 참고용으로만 사용하십시오.
이 회차에서 반드시 가져갈 것
| 지점 | 기억할 것 |
|---|---|
| 순서 규칙 | 인코딩은 분리 전, 스케일링은 분리 후 |
| 재대입 | fillna·drop·get_dummies는 새 객체를 반환 — df = df.… |
| 불균형 신호 | value_counts(normalize=True)가 7:3 이상으로 기울면 stratify=y + classification_report |
| 이진분류 짝 | Dense(1,'sigmoid') ↔ binary_crossentropy — 언제나 함께 바꾼다 |
| 콜백 | restore_best_weights=True와 callbacks=[es, mc] 두 곳을 모두 확인 |
| 예측 변환 | 이진분류는 (pred > 0.5).astype(int).flatten() — argmax 아님 |
| 최종 판단 | accuracy 0.7467 vs 전부 0 예측 0.7044 — 차이가 4%p뿐이라면 accuracy는 지표로서 무의미 |
한 문장으로: 이 회차의 진짜 문제는 모델이 아니라 지표를 고르는 눈입니다. 정확도 0.75를 보고 만족하는 답안과, 클래스 1의 recall 0.50을 짚어내는 답안의 차이가 이 시험이 보려는 것입니다.