Chapter 00. 이 책을 읽기 전에

이 책은 JavaScript에 능숙한 개발자가 Python을 최단 경로로 익혀 AICE 자격증 실기를 통과하도록 돕습니다. 문법을 처음부터 가르치지 않습니다 — 당신이 이미 아는 JS 개념 위에 Python의 차이점만 얹습니다.

0.1 이 책의 목적

시중의 Python 입문서는 변수·반복문·함수를 백지에서 설명합니다. 시니어 JS 개발자에게 그 지면은 낭비입니다. 이 책은 "map/filter/클로저/프로토타입을 설명 없이 이해하는 독자"를 전제로, JS와 다른 지점과 Python 고유 관용구에만 집중합니다.

목표는 두 가지입니다. 첫째, pandas·scikit-learn·Keras 코드를 읽고 쓸 수 있는 Python 문법 토대를 세웁니다. 둘째, 그 토대 위에서 AICE 실기 파이프라인(EDA → 전처리 → 모델링 → 평가)을 손에 익힙니다.

0.2 AICE 소개와 트랙 구분

AICE(AI Certification for Everyone)는 KT가 주관하는 국내 AI 활용 능력 자격증입니다. 여러 등급 중 이 책이 겨냥하는 것은 Python 실기 비중이 높은 두 트랙입니다.

트랙대상이 책의 커버 범위
Associate (A)실무자·개발자1~18장 (Part 1 + Part 2)
Professional (P)AI 개발자1~24장 (전체)

두 트랙 모두 Jupyter 환경에서 빈 셀에 코드를 채워 실행시키는 실기 시험입니다. 각 장에는 해당 문법이 시험에 어떻게 나오는지 알려주는 🎯 AICE 콜아웃이 있습니다.

0.3 대상 독자

이 책은 다음 독자를 위해 쓰였습니다.

만약 프로그래밍 자체가 처음이라면 이 책은 맞지 않습니다. 이 책의 모든 설명은 "JS에서는 이랬는데 Python에서는?"이라는 질문에 답하는 방식으로 전개됩니다.

0.4 이 책의 구성

본문 24개 장과 부록 3개로 이루어집니다.

파트장내용
Part 11~12장Python 문법 — JS 개발자의 눈으로
Part 213~18장AICE Associate 실전 — 데이터 핸들링과 모델링
Part 319~24장AICE Professional 심화 — 고급 ML·딥러닝
부록 A—JS → Python 치트시트
부록 B—AICE 실기 팁과 자주 하는 실수
부록 C—Associate vs Professional 로드맵

Part 1은 JS 대응이 명확한 개념을 빠르게 통과하고, Python 고유 관용구(컴프리헨션·슬라이싱·언패킹)와 JS 습관이 역효과를 내는 함정(truthiness·가변 기본값·늦은 바인딩)에 분량을 집중합니다. Part 2·3은 JS에 대응이 없는 벡터화 사고방식이 최대 전환점입니다.

0.5 이 책의 특징 — 읽는 방법

이 책에는 독자를 돕는 몇 가지 고유한 장치가 있습니다. 미리 알아두면 학습 효율이 올라갑니다.

JS ↔ Python 나란히 비교. 새 개념은 항상 두 언어를 나란히 놓은 표로 시작합니다. 왼쪽 JS는 이미 아는 내용이니, 오른쪽 Python이 어떻게 다른지만 확인하면 됩니다.

두 종류의 함정 콜아웃. 위험 지점은 눈에 띄게 표시하며, 성격에 따라 라벨을 둘로 나눕니다.

⚠️ JS 함정: JS 습관이나 문법 유추가 잘못된 Python 코드로 이어지는 지점입니다(주로 Part 1). 예를 들어 빈 리스트 []는 JS에서 truthy였지만 Python에서는 falsy입니다.
⚠️ 함정: JS와는 무관한, Python이나 라이브러리 자체의 함정입니다(주로 Part 2·3). 예를 들어 pandas에서 NaN == NaN이 False인 것은 JS 경험과 상관없이 알아야 하는 지점입니다.

시간이 급하다면 ⚠️ JS 함정만 골라 읽어도 JS→Python 전환 비용을 최소화할 수 있습니다. ⚠️ 함정은 해당 라이브러리를 다루는 장에서 확인하면 됩니다.

AICE 콜아웃. 해당 문법이 시험에 등장하는 형태를 짚어줍니다.

🎯 AICE: 이 문법이 실기 빈칸 채우기 문제에서 어떤 형태로 나오는지 알려줍니다.

빈칸 채우기형 연습문제. 각 장 끝에는 AICE 실기 형식을 본뜬 빈칸(____) 채우기 문제가 있습니다. 정답은 접힌 <details> 블록 안에 있으니, 먼저 풀어본 뒤 펼쳐 확인하세요.

모든 실행 예제는 실제로 검증되었습니다. 본문의 실행 가능한 Python 코드 블록(총 221개)은 실제 인터프리터로 실행해 출력까지 대조했습니다. # 출력: 주석은 진짜 실행 결과입니다. 복사해 그대로 돌려보아도 동작합니다. 단, 설치가 필요한 딥러닝·시각화 예제 등 일부는 실행 대신 읽기용으로 표시(no-run)했습니다.

0.6 실습 환경 안내

Python 3.13 기준으로 작성했습니다. AICE 실기가 Jupyter 노트북 환경이므로, 학습도 Jupyter(JupyterLab 또는 노트북)에서 진행하기를 권장합니다. 셀 단위 실행 모델은 1장에서 자세히 다룹니다.

패키지는 다음과 같이 나뉩니다.

가상환경을 만든 뒤 아래처럼 설치합니다(가상환경 개념은 1장 참조).

python3 -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install numpy pandas scikit-learn
pip install matplotlib seaborn tensorflow   # 시각화·딥러닝 장용
pip install xgboost lightgbm                # 선택 (20장 일부)

설치 없이 바로 시작하려면 브라우저에서 도는 Google Colab을 써도 됩니다. numpy·pandas·scikit-learn·tensorflow가 기본 제공되어 별도 설치가 거의 필요 없습니다.

0.7 트랙별 학습 방법

Associate 목표라면 1~18장이면 충분합니다. Professional 목표라면 1~24장 전체를 이수합니다. 두 트랙 모두 Part 1 문법이 토대이므로 건너뛰지 마세요.

JS 개발자에게 권하는 접근법은 하나입니다 — 아는 것은 대응표만 훑고, 함정에 시간을 쓰세요. JS와 거의 같은 부분(문자열 메서드, 모듈 import, 예외 처리)은 나란히 비교표만 확인하고 넘어가면 됩니다. 대신 ⚠️ 콜아웃이 붙은 지점(truthiness·is None·슬라이싱·삼항 어순·self·가변 기본값)은 반드시 예제를 직접 실행하며 몸에 익히세요. 시험에서 실점하는 곳은 몰라서가 아니라 JS 습관이 튀어나오는 곳입니다.

각 장의 선수 관계와 트랙별 최단 경로, 챕터 의존 그래프는 부록 C에 정리해 두었습니다. 학습 순서를 짤 때 부록 C를 지도로 삼으세요.

이제 1장에서 Python 실행 환경을 node 경험에 매핑하는 것으로 시작합니다.

Chapter 01. 개발 환경과 실행 모델

학습 목표
- python/pip/venv를 node/npm/nvm 경험에 매핑해 설명할 수 있다
- REPL과 스크립트 실행, 그리고 Jupyter 노트북 셀 실행 모델의 차이를 이해할 수 있다
- import 경로가 어떻게 해석되는지, 스크립트와 모듈이 어떻게 다른지 구분할 수 있다

1.1 런타임과 패키지 매니저 — node/npm의 대응물

JS에서 node app.js로 스크립트를 돌리고 npm install로 의존성을 받던 흐름을 그대로 옮겨봅니다. 이름만 바뀔 뿐 역할은 거의 1:1로 대응합니다.

JavaScriptPython
node app.jspython app.py
npm install requestspip install requests
node -v / nvm use 20python --version / pyenv

node처럼 python이 인터프리터입니다. 단, 여러 버전이 깔린 시스템에서는 python이 구버전(2.x)을 가리킬 수 있어, 실무에서는 python3을 명시하는 습관이 안전합니다.

import sys
print(sys.version_info.major)
# 출력: 3
🎯 AICE: 실기 환경은 Python 3.x가 설치된 Jupyter입니다. 버전 확인은 import sys; sys.version 또는 !python --version으로 합니다.

1.2 가상환경 — node_modules 대신 venv

JS는 node_modules가 프로젝트 폴더에 격리됩니다. Python은 전역 설치가 기본이라, 프로젝트별 격리를 위해 가상환경(virtual environment)을 명시적으로 만듭니다.

# 셸에서 실행 (프로젝트별 의존성 격리)
python3 -m venv .venv          # .venv 폴더 생성 (node_modules 대응)
source .venv/bin/activate      # 활성화 (Windows: .venv\Scripts\activate)
pip install pandas             # 이제 .venv 안에만 설치됨

package.json + package-lock.json에 대응하는 것은 requirements.txt입니다. pip freeze > requirements.txt로 만들고 pip install -r requirements.txt로 복원합니다.

⚠️ JS 함정: venv를 활성화하지 않고 pip install하면 전역 파이썬을 오염시킵니다. node_modules처럼 자동 격리되지 않으므로 activate를 잊지 마세요.

1.3 REPL — node 대화형 셸과 동일

인자 없이 python3을 실행하면 node의 대화형 REPL과 똑같은 프롬프트가 뜹니다. 표현식을 입력하면 값이 바로 출력됩니다.

# REPL에서 한 줄씩 입력한다고 상상하세요
2 ** 10          # node라면 2 ** 10, 결과는 동일
result = 2 ** 10
print(result)
# 출력: 1024

REPL에서는 print 없이 표현식만 쳐도 값이 표시되지만, 스크립트(.py 파일)에서는 print로 명시 출력해야 합니다. 이 차이는 다음 절의 Jupyter로 이어집니다.

1.4 Jupyter 노트북 셀 실행 모델

AICE 실기의 핵심 환경입니다. Jupyter는 REPL을 웹에 올린 형태로, 코드를 셀(cell) 단위로 나눠 실행합니다. 각 셀은 실행 순서를 공유하는 하나의 네임스페이스에서 위에서 아래로 돌아갑니다.

# [셀 1] 변수를 정의하면
tax_rate = 0.1

# [셀 2] 다음 셀에서 그대로 참조됩니다 (전역 네임스페이스 공유)
price = 1000
total = price * (1 + tax_rate)
print(total)   # Jupyter 셀이라면 마지막 줄에 total만 써도 표시됩니다
# 출력: 1100.0

셀의 마지막 표현식은 자동으로 표시되는 것이 REPL과 같지만, 셀 실행 순서를 사람이 임의로 바꿀 수 있다는 점이 함정입니다.

⚠️ JS 함정: 셀을 위→아래 순서가 아니라 실행한 순서대로 상태가 쌓입니다. 아래 셀을 먼저 실행하고 위 셀을 나중에 실행하면, 코드 순서와 무관하게 마지막 실행 값이 남습니다. 채점 전 반드시 "Restart & Run All"로 위에서부터 다시 돌려 재현성을 확인하세요.
🎯 AICE: 실기는 셀 곳곳의 ____ 빈칸을 채운 뒤 위에서부터 순서대로 실행해 오류 없이 통과하는지로 채점됩니다. 셀 간 변수 공유를 전제로 문제가 이어집니다.

1.5 import 경로와 스크립트 vs 모듈

JS의 import { x } from './m.js'처럼 Python도 파일을 모듈로 불러옵니다. 어순이 반대(from m import x)라는 점은 10장에서 자세히 다루고, 여기서는 실행 모델만 봅니다.

JavaScriptPython
import { add } from './util.js'from util import add
require('./util')import util

같은 .py 파일이 직접 실행될 때와 import될 때를 구분하는 관용구가 있습니다. JS의 require.main === module 패턴과 정확히 대응합니다.

# 이 파일이 python xxx.py로 직접 실행되면 __name__ == "__main__"
# 다른 파일이 import하면 __name__ == 모듈 이름
if __name__ == "__main__":
    print("스크립트로 직접 실행됨")
# 출력: 스크립트로 직접 실행됨

검증 환경은 이 코드를 __main__으로 실행하므로 위 분기가 참이 됩니다. 모듈로 import하면 이 블록은 건너뜁니다.

⚠️ JS 함정: import 경로는 실행 위치(현재 작업 디렉터리)와 sys.path를 기준으로 해석됩니다. JS의 상대경로(./)처럼 파일 기준이 아니어서, 다른 폴더에서 실행하면 ModuleNotFoundError가 날 수 있습니다.

연습문제

Q1. 현재 파이썬 버전의 major 번호(예: 3)를 출력하도록 빈칸을 채우세요.

import sys
print(sys.____.major)
정답
import sys
print(sys.version_info.major)
# 출력: 3

sys.version_info는 (major, minor, micro, ...) 형태의 네임드 튜플입니다.

Q2. 이 파일이 직접 실행될 때만 메시지를 출력하는 관용구입니다. 빈칸을 채우세요.

if __name__ == "____":
    print("main")
정답
if __name__ == "__main__":
    print("main")
# 출력: main

JS의 require.main === module에 대응하는 스크립트 진입점 관용구입니다.

이 챕터의 JS→Python 대응 한눈에 보기

JSPython비고
node app.jspython app.py인터프리터 직접 실행
npm install Xpip install X가상환경 활성화 후 실행
node_modules.venv (venv)자동 격리 아님 — 명시적 생성·활성화
package.json 의존성requirements.txtpip freeze로 생성
require.main === moduleif __name__ == "__main__":스크립트 진입점
REPL(node)REPL(python3) / Jupyter 셀마지막 표현식 자동 표시

Chapter 02. 변수·타입·연산자

학습 목표
- Python의 동적 타이핑과 변수 할당(상수 관례 포함)을 JS 대응으로 설명할 수 있다
- ==와 is, 그리고 is None 관용구를 올바르게 구분해 쓸 수 있다
- /와 //, 그리고 JS와 반대로 동작하는 truthiness 함정을 피할 수 있다

2.1 동적 타이핑과 변수 할당

JS의 let x = 1처럼 Python도 타입을 미리 선언하지 않습니다. 다만 let/const 키워드 없이 이름에 바로 할당합니다.

JavaScriptPython
let count = 3;count = 3
const MAX = 100;MAX = 100 # 관례상 대문자
typeof counttype(count)

Python에는 const가 없습니다. 상수는 대문자 이름이라는 관례로만 표현하며, 언어가 재할당을 막지는 않습니다.

count = 3
count = "three"   # 다른 타입 재할당도 허용 (동적 타이핑)
print(type(count).__name__)
# 출력: str
⚠️ JS 함정: const가 없으므로 대문자 상수도 실수로 재할당됩니다. 불변을 강제하고 싶다면 튜플(4장)이나 언어 관례에 의존해야 합니다.

2.2 == vs is — 값 비교와 정체성 비교

JS의 ===는 값과 타입을 함께 비교했습니다. Python의 ==가 그 역할(값 비교)이고, is는 같은 객체인지(정체성)를 봅니다. 둘을 혼동하면 미묘한 버그가 납니다.

JavaScriptPython
a === ba == b # 값 비교
a === b(동일 참조)a is b # 정체성 비교
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b)   # 값이 같은가?
print(a is b)   # 같은 객체인가?
# 출력: True

a == b는 True(내용이 같음)지만 a is b는 False(서로 다른 리스트 객체)입니다.

⚠️ JS 함정: 값을 비교하고 싶을 땐 언제나 ==입니다. is를 값 비교에 쓰면 작은 정수·짧은 문자열에서는 우연히 맞다가, 다른 값에서 갑자기 틀립니다.

2.3 is None — == null 습관을 버리기

JS에서 x == null로 null/undefined를 한 번에 잡던 패턴은 Python에 없습니다. Python의 "값 없음"은 None 하나뿐이고, 이것과의 비교는 관용적으로 is를 씁니다.

JavaScriptPython
if (x == null)if x is None:
if (x != null)if x is not None:
value = None
print(value is None)
print(value is not None)
# 출력: True

None은 프로그램 전체에 단 하나만 존재하는 싱글턴이라 정체성 비교(is)가 가장 정확하고 빠릅니다.

🎯 AICE: 결측치 처리(15장)에서 df['col'].isnull()을 쓰지만, 순수 파이썬 값의 존재 확인은 is None이 표준입니다. == None은 관례에 어긋나 감점 요소가 될 수 있습니다.

2.4 나눗셈: /와 //

JS의 /는 항상 실수 나눗셈이었습니다. Python의 /도 항상 float를 반환합니다(정수끼리 나눠도). 대신 정수 나눗셈용 // 연산자가 새로 있습니다.

JavaScriptPython
7 / 2 // 3.57 / 2 # 3.5
Math.floor(7 / 2)7 // 2 # 3
7 % 27 % 2 # 1
print(7 / 2)     # 항상 float
print(7 // 2)    # 정수 몫(내림)
print(7 % 2)     # 나머지
# 출력: 3.5

//는 몫을 내림(floor)합니다. 음수에서는 -7 // 2 == -4처럼 0쪽이 아니라 아래로 내려가니 주의합니다.

⚠️ JS 함정: 5 / 2가 2가 아니라 2.5인 건 JS와 같지만, 정수 결과가 필요할 때 Math.floor 대신 //를 쓴다는 점이 새롭습니다. 인덱스 계산 등에서 자주 씁니다.

2.5 truthiness — JS와 반대로 동작하는 함정

가장 위험한 차이입니다. JS에서 빈 배열 []과 빈 객체 {}는 truthy였습니다. Python에서 빈 컬렉션은 모두 falsy입니다.

값JavaScriptPython
[] / {}truthyfalsy
"" / 0falsyfalsy
None / nullfalsyfalsy
items = []
if items:
    print("데이터 있음")
else:
    print("비어 있음")
# 출력: 비어 있음

JS 습관대로 if (arr)로 "배열이 존재하는가"를 검사하면, Python에서는 빈 리스트일 때 분기가 반대로 갑니다.

⚠️ JS 함정: if items:는 "items가 비어있지 않은가"를 뜻합니다. "변수가 존재/할당되었는가"를 확인하려면 if items is not None:을 써야 빈 리스트와 None을 구분할 수 있습니다.
# 존재 여부와 비어있음을 구분해야 할 때
data = []
print("할당됨" if data is not None else "None임")   # 삼항 어순은 5장에서
# 출력: 할당됨

연습문제

Q1. 변수 user가 None인지 관용적으로 검사하는 조건을 채우세요.

user = None
if user ____ None:
    print("사용자 없음")
정답
user = None
if user is None:
    print("사용자 없음")
# 출력: 사용자 없음

None 비교는 ==가 아니라 is를 쓰는 것이 표준 관용구입니다.

Q2. 두 정수의 몫(정수)과 나머지를 각각 구하도록 연산자를 채우세요.

q = 17 ____ 5    # 몫: 3
r = 17 ____ 5    # 나머지: 2
print(q, r)
정답
q = 17 // 5
r = 17 % 5
print(q, r)
# 출력: 3 2

/는 float(3.4)를 주므로 정수 몫에는 //를 씁니다.

Q3. 리스트가 비어 있으면 "empty"를 출력합니다. Python truthiness에 맞게 채우세요.

nums = []
if ____ nums:
    print("empty")
정답
nums = []
if not nums:
    print("empty")
# 출력: empty

빈 리스트는 falsy이므로 not nums가 True입니다.

이 챕터의 JS→Python 대응 한눈에 보기

JSPython비고
let / const그냥 할당 / 대문자 관례const 없음 — 재할당 막지 못함
=== (값+타입)==값 비교
동일 참조 비교is정체성(같은 객체) 비교
x == nullx is NoneNone은 싱글턴
Math.floor(a/b)a // b정수 나눗셈(내림)
[]/{} truthy[]/{} falsy빈 컬렉션 분기 반대

Chapter 03. 문자열과 포매팅

학습 목표
- f-string으로 값을 삽입하고 포맷 스펙(:.2f 등)으로 출력 형식을 제어할 수 있다
- split/join/strip/replace 등 핵심 문자열 메서드를 JS 대응으로 쓸 수 있다
- 문자열 슬라이싱의 기본 문법을 이해한다(4장에서 심화)

3.1 f-string — 템플릿 리터럴의 대응물

JS의 템플릿 리터럴 ` ${x} 에 정확히 대응하는 것이 f-string입니다. 문자열 앞에 f를 붙이고 {}` 안에 표현식을 씁니다.

JavaScriptPython
` Hi ${name} `f"Hi {name}"
` ${a + b} `f"{a + b}"
name = "Ada"
age = 36
print(f"{name} is {age}")
print(f"next year: {age + 1}")   # 중괄호 안에 표현식도 가능
# 출력: Ada is 36
⚠️ JS 함정: 백틱이 아니라 따옴표를 씁니다. f를 빼먹으면 그냥 "{name}" 리터럴 문자열이 출력됩니다. JS에서 ${}를 백틱 없는 문자열에 넣으면 치환 안 되던 것과 같은 실수입니다.

3.2 포맷 스펙 — 템플릿 리터럴보다 강력한 지점

f-string은 {값:포맷} 형태로 자릿수·정렬·천단위 구분을 선언적으로 지정합니다. JS라면 toFixed, padStart, toLocaleString을 조합해야 하던 일이 한 줄에 들어갑니다.

JavaScriptPython
x.toFixed(2)f"{x:.2f}"
n.toLocaleString()f"{n:,}"
s.padStart(5)f"{s:>5}"
pi = 3.14159
price = 1234567
print(f"{pi:.2f}")      # 소수점 2자리
print(f"{price:,}")     # 천단위 콤마
print(f"{price:,.1f}")  # 콤마 + 소수 1자리
# 출력: 3.14

:.2f는 반올림하여 소수 둘째 자리까지 고정 표기합니다. AICE에서 평가지표(R², RMSE 등)를 정해진 자릿수로 출력할 때 자주 씁니다.

🎯 AICE: 모델 성능을 print(f"정확도: {acc:.2f}")처럼 포맷해 제출하는 문항이 나옵니다. :.2f, :.4f 포맷 스펙을 손에 익혀두세요.

3.3 문자열 메서드 — split / join / strip / replace

이름과 시그니처만 다를 뿐 JS 메서드와 거의 대응합니다. 가장 큰 차이는 join의 주어가 구분자 문자열이라는 점입니다.

JavaScriptPython
"a,b".split(",")"a,b".split(",")
arr.join("-")"-".join(arr)
s.trim()s.strip()
s.replaceAll("a","b")s.replace("a","b")
csv = "  ada, grace , alan  "
names = [n.strip() for n in csv.split(",")]   # 컴프리헨션은 6장
print(names)
joined = "-".join(names)
print(joined)
# 출력: ['ada', 'grace', 'alan']
⚠️ JS 함정: arr.join("-")가 아니라 "-".join(arr)입니다. 구분자가 메서드의 주인입니다. 어순을 뒤집으면 AttributeError가 납니다.

Python의 replace는 JS의 replaceAll처럼 모든 일치를 바꿉니다. 첫 번째만 바꾸려면 세 번째 인자로 횟수를 줍니다.

s = "a-b-c-d"
print(s.replace("-", "/"))       # 전부 치환
print(s.replace("-", "/", 1))    # 1개만
# 출력: a/b/c/d

3.4 슬라이싱 맛보기

문자열은 문자의 시퀀스라, 인덱스와 슬라이스로 잘라낼 수 있습니다. JS의 slice와 비슷하지만 [start:stop:step] 대괄호 문법을 씁니다. 4장에서 리스트와 함께 깊이 다룹니다.

JavaScriptPython
s[0]s[0]
s.slice(0, 3)s[0:3]
s.slice(-2)s[-2:]
word = "python"
print(word[0])       # 첫 글자
print(word[0:3])     # 0,1,2번 문자
print(word[-1])      # 마지막 글자(음수 인덱스)
print(word[::-1])    # 뒤집기
# 출력: p
⚠️ JS 함정: 음수 인덱스 word[-1]이 마지막 글자입니다. JS는 s[-1]이 undefined였지만 Python은 뒤에서부터 셉니다. s.at(-1)에 대응한다고 보면 됩니다.

연습문제

Q1. 실수 score를 소수점 셋째 자리까지 출력하도록 포맷 스펙을 채우세요.

score = 0.87654
print(f"{score:____}")
정답
score = 0.87654
print(f"{score:.3f}")
# 출력: 0.877

.3f는 반올림하여 소수 셋째 자리까지 고정 표기합니다.

Q2. 리스트 parts를 언더스코어로 이어 붙이세요.

parts = ["2024", "01", "15"]
result = "____".____(parts)   # "2024_01_15"
정답
parts = ["2024", "01", "15"]
result = "_".join(parts)
print(result)
# 출력: 2024_01_15

구분자 문자열이 join의 주인입니다(parts.join이 아님).

이 챕터의 JS→Python 대응 한눈에 보기

JSPython비고
` ${x} `f"{x}"백틱 아닌 따옴표 + f 접두사
x.toFixed(2)f"{x:.2f}"포맷 스펙이 더 강력
n.toLocaleString()f"{n:,}"천단위 구분
arr.join("-")"-".join(arr)구분자가 주어
s.trim()s.strip()양끝 공백 제거
s.replaceAll(a,b)s.replace(a,b)기본이 전체 치환
s.at(-1)s[-1]음수 인덱스 지원

Chapter 04. 컬렉션: list·dict·set·tuple

학습 목표
- list·dict·set·tuple 4대 컬렉션을 JS 자료구조에 매핑해 쓸 수 있다
- [start:stop:step]과 [::-1] 슬라이싱을 자유롭게 다룰 수 있다(pandas 인덱싱 선수 지식)
- in 멤버십 연산과 튜플의 불변성·언패킹을 이해한다

4.1 list — Array의 대응물

JS 배열과 거의 같습니다. 다만 메서드 이름이 다릅니다 — 특히 push가 아니라 append입니다.

JavaScriptPython
arr.push(4)lst.append(4)
arr.pop()lst.pop()
arr.lengthlen(lst)
arr.includes(3)3 in lst
nums = [1, 2, 3]
nums.append(4)        # push 아님!
nums.pop()            # 마지막 제거
print(len(nums), nums)
# 출력: 3 [1, 2, 3]
⚠️ JS 함정: push는 Python에 없습니다. append입니다 — JS 개발자 최다 오타. 길이도 .length 속성이 아니라 len(lst) 함수입니다.

4.2 슬라이싱 — [start:stop:step]

Python 컬렉션의 핵심 무기입니다. JS의 slice보다 훨씬 강력하며, pandas의 iloc 인덱싱이 이 문법을 그대로 씁니다. 철저히 익혀둡니다.

JavaScriptPython
arr.slice(1, 4)lst[1:4]
arr.slice(-2)lst[-2:]
[...arr].reverse()lst[::-1]
xs = [0, 10, 20, 30, 40, 50]
print(xs[1:4])     # 인덱스 1,2,3 (stop 미포함)
print(xs[:3])      # 처음부터 3개
print(xs[3:])      # 3번부터 끝까지
print(xs[-2:])     # 마지막 2개
# 출력: [10, 20, 30]

stop은 포함되지 않습니다(JS slice와 동일). 세 번째 값 step은 건너뛰기 간격입니다.

xs = [0, 10, 20, 30, 40, 50]
print(xs[::2])     # 2칸씩 (짝수 인덱스)
print(xs[1::2])    # 1부터 2칸씩 (홀수 인덱스)
print(xs[::-1])    # 역순 (step -1)
# 출력: [0, 20, 40]
⚠️ JS 함정: lst[::-1]이 리스트를 뒤집는 관용구입니다. JS의 reverse()는 원본을 in-place로 뒤집었지만, 슬라이싱은 새 리스트를 반환하고 원본은 그대로 둡니다. 복사(lst[:])로도 자주 씁니다.
🎯 AICE: df.iloc[0:5], arr[:, 0] 같은 pandas·NumPy 인덱싱이 이 슬라이싱 문법의 확장입니다(13장, 14장). 여기서 손에 익히지 않으면 Part 2에서 막힙니다.

4.3 dict — Object/Map의 대응물

JS 객체와 Map을 합친 역할입니다. 키로 임의 타입을 쓸 수 있어 Map에 가깝지만, 리터럴 문법은 객체와 닮았습니다.

JavaScriptPython
{ name: "ada" }{"name": "ada"}
obj.name / obj["name"]d["name"]
Object.keys(obj)d.keys()
Object.entries(obj)d.items()
user = {"name": "ada", "age": 36}
print(user["name"])        # 대괄호 접근 (점 접근 없음)
user["email"] = "a@x.io"   # 추가
print(list(user.keys()))
# 출력: ada
⚠️ JS 함정: user.name 같은 점 접근은 안 됩니다. 언제나 user["name"] 대괄호입니다. 키 문자열도 반드시 따옴표로 감쌉니다({name: ...} 아님).

없는 키를 대괄호로 접근하면 KeyError가 납니다. JS는 undefined를 줬지만 Python은 예외를 던지므로, 안전 접근엔 get을 씁니다.

user = {"name": "ada"}
print(user.get("age"))          # 없으면 None (KeyError 아님)
print(user.get("age", 0))       # 기본값 지정
# 출력: None

4.4 set — Set의 대응물

중복 없는 모음. JS Set과 대응하지만 리터럴 {}과 집합 연산자를 지원합니다.

JavaScriptPython
new Set([1,1,2]){1, 1, 2}
set.has(x)x in s
set.add(x)s.add(x)
a = {1, 2, 3}
b = {2, 3, 4}
print(a & b)    # 교집합
print(a | b)    # 합집합
print(a - b)    # 차집합
# 출력: {2, 3}
⚠️ JS 함정: 빈 중괄호 {}는 set이 아니라 빈 dict입니다. 빈 set은 set()으로 만듭니다.

4.5 tuple — JS에 없는 불변 시퀀스

Python 고유 개념입니다. list와 같지만 불변(immutable)이라 생성 후 수정할 수 없습니다. 소괄호로 만들며, 함수의 다중 반환·언패킹에 광범위하게 쓰입니다.

point = (3, 4)       # 불변 시퀀스
print(point[0])      # 인덱싱은 됨
# point[0] = 9       # TypeError — 수정 불가
x, y = point         # 언패킹 (JS 구조분해와 유사)
print(x, y)
# 출력: 3

JS의 배열 구조분해 const [a, b] = arr가 Python의 튜플 언패킹에 대응합니다. 두 값 교환도 임시 변수 없이 한 줄입니다.

a, b = 1, 2
a, b = b, a          # 스왑 (JS: [a, b] = [b, a])
print(a, b)
# 출력: 2 1
⚠️ JS 함정: 함수가 여러 값을 반환하면 사실 튜플 하나를 반환하는 것입니다. return x, y는 return (x, y)와 같고, 받는 쪽에서 a, b = f()로 언패킹합니다.

4.6 in 멤버십 — 자료구조 공통

includes/has/in(객체 키)로 나뉘던 JS와 달리, Python은 모든 컬렉션에 in 하나로 통일됩니다.

print(3 in [1, 2, 3])            # 리스트: 값 존재?
print("name" in {"name": "a"})   # dict: 키 존재?
print("y" in "python")           # 문자열: 부분 문자열?
# 출력: True
⚠️ JS 함정: dict에 in을 쓰면 키를 검사합니다(값이 아님). JS의 key in obj와 같고, 값 검사는 v in d.values()로 합니다.

연습문제

Q1. 리스트 xs를 역순으로 뒤집는 슬라이싱을 채우세요.

xs = [1, 2, 3, 4]
reversed_xs = xs[____]   # [4, 3, 2, 1]
정답
xs = [1, 2, 3, 4]
reversed_xs = xs[::-1]
print(reversed_xs)
# 출력: [4, 3, 2, 1]

step을 -1로 주면 끝에서부터 훑어 역순 리스트를 새로 만듭니다.

Q2. dict에서 없을 수도 있는 키를 안전하게(예외 없이) 꺼내고, 없으면 0을 반환하세요.

counts = {"a": 3}
value = counts.____("b", 0)   # 0
정답
counts = {"a": 3}
value = counts.get("b", 0)
print(value)
# 출력: 0

get은 키가 없으면 KeyError 대신 기본값(여기선 0)을 반환합니다.

Q3. 두 변수 값을 임시 변수 없이 교환하세요.

a, b = 10, 20
____ = b, a
print(a, b)   # 20 10
정답
a, b = 10, 20
a, b = b, a
print(a, b)
# 출력: 20 10

우변이 튜플 (b, a)로 묶였다가 좌변으로 언패킹됩니다.

이 챕터의 JS→Python 대응 한눈에 보기

JSPython비고
arr.push(x)lst.append(x)push 없음
arr.lengthlen(lst)함수 호출
arr.slice(1,4)lst[1:4]stop 미포함
[...arr].reverse()lst[::-1]새 리스트 반환
{name:"a"}{"name":"a"}키에 따옴표
obj["k"](없음→undefined)d["k"](없음→KeyError)안전 접근은 d.get(k)
new Set([...]){...} / set()빈 set은 set()
(없음)tuple (x, y)불변 시퀀스·언패킹
includes/has/inin컬렉션 공통

Chapter 05. 제어 흐름

학습 목표
- 들여쓰기 기반 블록 구조와 "블록 스코프 없음"을 이해할 수 있다
- for ... in, range, enumerate, zip으로 Python식 순회를 작성할 수 있다
- 삼항 표현식의 어순(값-조건-값)과 while을 올바르게 쓸 수 있다

5.1 들여쓰기 블록 — 중괄호가 없다

Python은 {} 대신 들여쓰기로 블록을 구분합니다. 조건문 끝에 콜론(:)을 붙이고 다음 줄을 들여씁니다. 세미콜론도 쓰지 않습니다.

JavaScriptPython
if (x > 0) { ... }if x > 0: + 들여쓰기
else ifelif
x = 7
if x > 10:
    print("큼")
elif x > 5:
    print("중간")
else:
    print("작음")
# 출력: 중간

else if가 아니라 elif입니다. 조건에 괄호는 선택 사항이라 보통 생략합니다.

⚠️ JS 함정: 들여쓰기가 문법입니다. 스페이스 4칸이 관례이며, 탭과 스페이스를 섞으면 IndentationError가 납니다. JS처럼 자유롭게 들여써도 되던 감각을 버려야 합니다.

5.2 블록 스코프가 없다

JS의 let은 if/for 블록 안에 갇혔지만, Python의 블록은 스코프를 만들지 않습니다. 블록 안에서 만든 변수가 블록 밖에서도 살아있습니다(함수 단위 스코프 — 8장에서 심화).

if True:
    leaked = 42        # 블록 안에서 정의했지만
print(leaked)          # 블록 밖에서도 접근됨
# 출력: 42
⚠️ JS 함정: if/for 블록은 스코프 경계가 아닙니다. for 루프가 끝난 뒤에도 루프 변수가 마지막 값으로 남아있습니다. let의 블록 격리를 기대하면 안 됩니다.

5.3 for ... in — for...of의 대응물

JS의 for...of가 Python의 기본 for입니다. 인덱스 기반 for(let i=0; ...)는 거의 쓰지 않고, 값을 직접 순회합니다.

JavaScriptPython
for (const x of arr)for x in arr:
for (const k in obj)for k in d: # 키 순회
for fruit in ["apple", "banana"]:
    print(fruit)
# 출력: apple
⚠️ JS 함정: JS의 for...in(키/인덱스)과 for...of(값)를 혼동하지 마세요. Python for x in은 항상 값을 줍니다(리스트면 원소, dict면 키).

5.4 range — 인덱스가 필요할 때

C 스타일 카운팅 루프가 필요하면 range를 씁니다. range(stop) 또는 range(start, stop, step) 형태로, stop은 포함하지 않습니다.

for i in range(3):        # 0, 1, 2
    print(i)
print(list(range(2, 10, 2)))   # start, stop, step
# 출력: 0

5.5 enumerate — 인덱스와 값을 함께

JS의 arr.forEach((v, i) => ...)나 arr.entries()에 대응합니다. enumerate는 (인덱스, 값) 쌍을 튜플로 내줍니다.

JavaScriptPython
arr.forEach((v, i)=>...)for i, v in enumerate(arr):
arr.entries()enumerate(arr)
colors = ["red", "green", "blue"]
for i, color in enumerate(colors):
    print(i, color)
# 출력: 0 red

시작 인덱스를 바꾸려면 enumerate(colors, start=1)처럼 둘째 인자를 줍니다.

5.6 zip — 여러 시퀀스를 나란히

여러 리스트를 인덱스로 맞춰 동시에 순회합니다. JS에는 내장이 없어 map으로 흉내 내던 패턴입니다.

names = ["ada", "alan"]
ages = [36, 41]
for name, age in zip(names, ages):
    print(f"{name}: {age}")
# 출력: ada: 36

zip은 가장 짧은 시퀀스 길이에 맞춰 멈춥니다. 두 리스트를 dict로 묶을 때도 dict(zip(keys, values))로 자주 씁니다.

🎯 AICE: enumerate와 zip은 컬럼 목록과 값 목록을 짝지어 순회하거나, 결과에 번호를 붙일 때 실기에서 자주 등장합니다.

5.7 삼항 표현식 — 어순이 뒤집힌다

JS의 cond ? a : b가 Python에서는 a if cond else b입니다. 참일 때 값이 맨 앞에 오는 값-조건-값 어순입니다.

JavaScriptPython
x > 0 ? "양" : "음""양" if x > 0 else "음"
score = 72
grade = "pass" if score >= 60 else "fail"
print(grade)
# 출력: pass
⚠️ JS 함정: 어순이 ?:와 다릅니다. "값을 먼저, 조건을 가운데" 순으로 읽습니다. 조건이 앞에 온다고 착각해 if score >= 60 "pass" ...처럼 쓰면 문법 오류입니다.

5.8 while

JS와 거의 동일합니다. 괄호가 없고 콜론+들여쓰기라는 점만 다릅니다. break/continue도 그대로입니다.

n = 0
while n < 3:
    print(n)
    n += 1        # Python엔 n++ 없음, += 사용
# 출력: 0
⚠️ JS 함정: n++, ++n 증감 연산자가 없습니다. n += 1을 씁니다.

연습문제

Q1. 리스트를 (번호, 값) 형태로 1번부터 출력하도록 채우세요.

items = ["a", "b", "c"]
for idx, val in ____(items, start=1):
    print(idx, val)
정답
items = ["a", "b", "c"]
for idx, val in enumerate(items, start=1):
    print(idx, val)
# 출력: 1 a

enumerate의 start로 시작 번호를 바꿉니다.

Q2. 점수가 90 이상이면 "A", 아니면 "B"를 삼항 표현식으로 배정하세요.

score = 95
grade = "A" ____ score >= 90 ____ "B"
정답
score = 95
grade = "A" if score >= 90 else "B"
print(grade)
# 출력: A

Python 삼항은 값 if 조건 else 값 어순입니다.

Q3. 두 리스트를 짝지어 dict로 만드세요.

keys = ["x", "y"]
vals = [1, 2]
d = dict(____(keys, vals))   # {"x": 1, "y": 2}
정답
keys = ["x", "y"]
vals = [1, 2]
d = dict(zip(keys, vals))
print(d)
# 출력: {'x': 1, 'y': 2}

zip이 만든 (키, 값) 쌍들을 dict가 딕셔너리로 조립합니다.

이 챕터의 JS→Python 대응 한눈에 보기

JSPython비고
if (...) { }if ...: + 들여쓰기중괄호·세미콜론 없음
else ifelif
블록 스코프(let)(없음)블록이 스코프 아님
for (const x of arr)for x in arr:값 순회
for(let i=0; ...)for i in range(n):stop 미포함
arr.forEach((v,i)=>)enumerate(arr)(인덱스, 값)
(없음)zip(a, b)여러 시퀀스 병렬
cond ? a : ba if cond else b어순 뒤집힘
n++n += 1증감 연산자 없음

Chapter 06. 컴프리헨션

학습 목표
- 리스트 컴프리헨션으로 map/filter 체인을 대체할 수 있다
- 조건부·중첩 컴프리헨션을 읽고 쓸 수 있다
- dict/set 컴프리헨션과 제너레이터 표현식의 차이를 이해한다

6.1 리스트 컴프리헨션 — map의 대체

JS에서 arr.map(f)로 변환하던 것을 Python은 컴프리헨션 문법으로 씁니다. [표현식 for 변수 in 반복가능] 형태입니다.

JavaScriptPython
nums.map(n => n * 2)[n * 2 for n in nums]
words.map(w => w.length)[len(w) for w in words]
nums = [1, 2, 3, 4]
doubled = [n * 2 for n in nums]
print(doubled)
# 출력: [2, 4, 6, 8]

for n in nums로 원소를 하나씩 꺼내 앞쪽 n * 2를 적용한 결과를 새 리스트로 모읍니다. 왼쪽이 "무엇을 담을지", 오른쪽이 "어디서 꺼낼지"입니다.

⚠️ JS 함정: map은 콜백 함수를 받았지만 컴프리헨션은 표현식을 직접 씁니다. return도 화살표도 없습니다. 함수 호출 오버헤드가 없어 더 빠르고, Python에서 이쪽이 관용적입니다.

6.2 조건부 컴프리헨션 — filter의 대체

뒤에 if 조건을 붙이면 filter 역할을 합니다. filter와 map을 한 줄에 합칠 수 있어 체이닝이 사라집니다.

JavaScriptPython
nums.filter(n => n > 0)[n for n in nums if n > 0]
nums.filter(n=>n>0).map(n=>n*2)[n * 2 for n in nums if n > 0]
nums = [-2, -1, 0, 1, 2]
positives = [n for n in nums if n > 0]
doubled_pos = [n * 2 for n in nums if n > 0]
print(positives)
print(doubled_pos)
# 출력: [1, 2]
🎯 AICE: 이 "필터+변환" 사고가 pandas 벡터화(df[df.col > 0]['col'] * 2)의 토대입니다. 반복문 대신 한 표현식으로 컬렉션을 변환하는 감각을 여기서 만들어야 Part 2가 수월합니다.

6.3 뒤쪽 if와 앞쪽 if-else 구분

if의 위치로 의미가 갈립니다. 뒤쪽 if는 원소를 걸러내고(filter), 앞쪽 if-else는 값을 고릅니다(삼항 표현식, 5장).

nums = [-2, -1, 1, 2]
# 뒤쪽 if: 음수를 걸러냄 (개수가 줄어듦)
kept = [n for n in nums if n > 0]
# 앞쪽 if-else: 모든 원소를 변환 (개수 유지)
signs = ["+" if n > 0 else "-" for n in nums]
print(kept)
print(signs)
# 출력: [1, 2]
⚠️ JS 함정: 걸러내기(뒤쪽 if)와 값 고르기(앞쪽 if-else)를 혼동하면 결과 개수가 달라집니다. "걸러내려면 뒤, 바꾸려면 앞"으로 기억하세요.

6.4 dict / set 컴프리헨션

같은 문법을 중괄호로 쓰면 dict나 set이 나옵니다. dict는 키: 값 쌍을, set은 단일 값을 담습니다.

JavaScriptPython
Object.fromEntries(arr.map(...)){k: v for k, v in pairs}
new Set(arr.map(...)){f(x) for x in arr}
words = ["ada", "alan", "grace"]
# dict 컴프리헨션: 단어 → 길이
lengths = {w: len(w) for w in words}
# set 컴프리헨션: 중복 없는 길이 집합
unique_lengths = {len(w) for w in words}
print(lengths)
print(unique_lengths)
# 출력: {'ada': 3, 'alan': 4, 'grace': 5}
⚠️ JS 함정: {w: len(w) ...}처럼 키: 값이면 dict, {len(w) ...}처럼 값 하나면 set입니다. 콜론 유무가 자료구조를 가릅니다.

6.5 제너레이터 표현식 — 지연 평가

대괄호 대신 소괄호를 쓰면 리스트가 아니라 제너레이터가 됩니다. 전체를 메모리에 만들지 않고 필요할 때 하나씩 계산합니다(12장에서 심화). 큰 데이터의 합계·최댓값 등에 유리합니다.

# 리스트를 만들지 않고 곧바로 합산 (메모리 절약)
total = sum(n * n for n in range(1, 5))   # sum() 인자로 직접 전달
print(total)
# 출력: 30

sum(), max(), any(), all() 같은 집계 함수에 제너레이터 표현식을 직접 넘기면 중간 리스트 없이 처리됩니다.

⚠️ JS 함정: 제너레이터는 한 번만 소비됩니다. 리스트처럼 재사용하려고 두 번 순회하면 두 번째는 비어 있습니다. 여러 번 쓸 값이면 리스트 컴프리헨션([...])으로 만드세요.

6.6 중첩 컴프리헨션

이중 반복문을 한 줄로 폅니다. for가 바깥→안 순서로 나열됩니다(중첩 for문을 위에서 아래로 읽는 순서 그대로).

matrix = [[1, 2], [3, 4]]
# 2차원 리스트를 1차원으로 평탄화
flat = [x for row in matrix for x in row]
print(flat)
# 출력: [1, 2, 3, 4]

for row in matrix(바깥)가 먼저, for x in row(안)가 나중입니다. 일반 중첩 for문을 그대로 옮겨 적은 순서라 생각하면 헷갈리지 않습니다.

⚠️ JS 함정: 중첩 for의 순서는 바깥이 왼쪽입니다. 결과를 만드는 표현식(x)만 맨 앞에 오고, 반복은 읽는 순서대로 이어 붙입니다. 너무 깊어지면 가독성이 나빠지니 2단까지만 권합니다.

연습문제

Q1. nums에서 짝수만 골라 제곱한 리스트를 컴프리헨션으로 만드세요.

nums = [1, 2, 3, 4, 5, 6]
result = [____ for n in nums ____ n % 2 == 0]   # [4, 16, 36]
정답
nums = [1, 2, 3, 4, 5, 6]
result = [n * n for n in nums if n % 2 == 0]
print(result)
# 출력: [4, 16, 36]

앞쪽 n * n이 변환, 뒤쪽 if n % 2 == 0이 짝수 필터입니다.

Q2. 단어 리스트를 {단어: 대문자단어} 딕셔너리로 만드세요.

words = ["ada", "alan"]
upper_map = {w: ____ for w in words}   # {'ada': 'ADA', 'alan': 'ALAN'}
정답
words = ["ada", "alan"]
upper_map = {w: w.upper() for w in words}
print(upper_map)
# 출력: {'ada': 'ADA', 'alan': 'ALAN'}

키: 값 형태라 dict 컴프리헨션입니다.

Q3. range(1, 101)의 제곱합을 중간 리스트 없이 구하세요.

total = sum(____ for n in range(1, 101))
정답
total = sum(n * n for n in range(1, 101))
print(total)
# 출력: 338350

소괄호 제너레이터 표현식을 sum에 직접 넘겨 메모리를 아낍니다.

이 챕터의 JS→Python 대응 한눈에 보기

JSPython비고
arr.map(f)[f(x) for x in arr]표현식 직접
arr.filter(p)[x for x in arr if p(x)]뒤쪽 if
arr.filter().map()[f(x) for x in arr if p(x)]한 줄로 합침
arr.map(x=>cond?a:b)[a if cond else b for x in arr]앞쪽 if-else
Object.fromEntries(...){k: v for ...}dict 컴프리헨션
new Set(arr.map(...)){f(x) for x in arr}set 컴프리헨션
(지연 평가 없음)(f(x) for x in arr)제너레이터, 1회 소비
이중 for + push[x for row in m for x in row]중첩(바깥이 왼쪽)

Chapter 07. 함수

학습 목표
- def로 함수를 정의하고 기본 인자·키워드 인자를 JS 대응으로 사용할 수 있다
- *args/**kwargs로 가변 인자를 받아 rest/spread 습관을 Python으로 옮길 수 있다
- lambda의 "한 줄 표현식" 제약을 이해하고 언제 def를 써야 할지 판단할 수 있다
- 가변 기본값(def f(x=[])) 공유 함정을 피하는 None 관용구를 쓸 수 있다

7.1 함수 정의 — def와 화살표 함수

JS에서 함수는 값이자 일급 객체입니다. Python도 똑같습니다. 다만 화살표 함수 같은 간결 문법이 없고, 모든 함수는 def 키워드로 정의합니다.

JavaScriptPython
``const add = (a, b) => a + b;`<br>`const r = add(2, 3);````def add(a, b):`<br>` return a + b`<br>`r = add(2, 3)``

가장 큰 차이는 세 가지입니다. (1) 화살표 함수의 암묵적 return이 없어 값을 돌려주려면 항상 return을 씁니다. (2) 블록은 중괄호가 아니라 들여쓰기입니다. (3) 함수도 값이므로 변수에 넣거나 인자로 넘길 수 있습니다.

def add(a, b):
    return a + b

def apply(func, x, y):
    return func(x, y)

print(apply(add, 2, 3))
# 출력: 5

return이 없으면 JS의 undefined가 아니라 None을 반환합니다.

def greet(name):
    print(f"안녕하세요, {name}님")

result = greet("Ada")
print(result)
# 출력: 안녕하세요, Ada님

7.2 기본 인자와 키워드 인자

JS의 기본 매개변수는 Python에도 있습니다. 여기에 더해 Python은 호출 시 인자 이름을 지정하는 키워드 인자를 언어 차원에서 지원합니다.

JavaScriptPython
``function f(a, b = 10) {}`<br>`f(1);````def f(a, b=10):`<br>` ...`<br>`f(1)``

키워드 인자를 쓰면 인자 순서와 무관하게, 이름으로 값을 전달할 수 있습니다. 가독성이 크게 올라가므로 인자가 3개 이상인 함수 호출에서 특히 유용합니다.

def connect(host, port=5432, timeout=30):
    return f"{host}:{port} (timeout={timeout})"

print(connect("db.local"))
# 출력: db.local:5432 (timeout=30)
print(connect("db.local", timeout=5))
# 출력: db.local:5432 (timeout=5)
print(connect("db.local", port=3306, timeout=5))
# 출력: db.local:3306 (timeout=5)
🎯 AICE: sklearn·keras API는 키워드 인자로 도배되어 있습니다. train_test_split(X, y, test_size=0.2, random_state=42), model.fit(X, y, epochs=10, batch_size=32)처럼 대부분의 하이퍼파라미터가 키워드 인자입니다. 빈칸 채우기 문제에서 random_state=____ 형태가 흔하므로 이름과 순서에 익숙해져야 합니다.

7.3 가변 인자 — *args와 **kwargs

JS의 rest 파라미터 ...args는 Python에서 두 갈래로 나뉩니다. 위치 인자를 모으는 *args(튜플)와 키워드 인자를 모으는 **kwargs(딕셔너리)입니다.

JavaScriptPython
``function sum(...nums) {}````def total(*nums):``
``f(...arr)`` (spread 호출)``f(*arr)`` (언패킹 호출)

args는 남는 위치 인자를 튜플로 모읍니다. 호출할 때 리스트 앞에 를 붙이면 반대로 펼쳐서 넘길 수 있습니다 — 이것이 JS spread의 대응입니다.

def total(*nums):
    return sum(nums)

print(total(1, 2, 3, 4))
# 출력: 10

values = [10, 20, 30]
print(total(*values))
# 출력: 60

**kwargs는 이름 붙은 인자를 딕셔너리로 모읍니다. 설정값을 유연하게 받는 함수에서 자주 씁니다.

def make_user(**kwargs):
    return kwargs

print(make_user(name="Ada", role="admin"))
# 출력: {'name': 'Ada', 'role': 'admin'}

config = {"host": "db.local", "port": 3306, "timeout": 5}
print(connect(**config))
# 출력: db.local:3306 (timeout=5)

딕셔너리 앞에 를 붙이면 키워드 인자로 펼쳐집니다. 위에서 connect(config)는 connect(host="db.local", port=3306, timeout=5)와 같습니다.

7.4 lambda — 화살표 함수의 축소판

JS 화살표 함수와 가장 닮은 것은 lambda입니다. 그러나 결정적 제약이 있습니다. lambda 본문은 단 하나의 표현식이어야 합니다. 문(statement)을 넣을 수 없으니 if문, 반복문, 여러 줄을 담지 못합니다.

JavaScriptPython
``const sq = x => x * x;````sq = lambda x: x * x``
``arr.sort((a,b) => a - b)````sorted(arr, key=lambda x: x)``

lambda는 주로 sorted, map, filter의 key/변환 함수처럼 "잠깐 쓰고 버릴" 자리에서 씁니다. 로직이 조금이라도 길어지면 미련 없이 def로 바꾸는 것이 Python다운 선택입니다.

people = [("Ada", 36), ("Bob", 28), ("Cid", 41)]
by_age = sorted(people, key=lambda person: person[1])
print(by_age)
# 출력: [('Bob', 28), ('Ada', 36), ('Cid', 41)]
⚠️ JS 함정: JS에서는 화살표 함수 본문에 { ... }로 여러 문장을 쓰지만, Python lambda는 그게 불가능합니다. lambda x: (print(x), x*2)처럼 억지로 쑤셔 넣지 말고, 두 줄 이상이면 def를 쓰세요. 삼항 표현식(a if cond else b)은 표현식이므로 lambda 안에서 사용할 수 있습니다.

7.5 가변 기본값 함정 — Python 최악의 함정

JS에는 없는, Python 초심자를 반드시 한 번은 무너뜨리는 함정입니다. 기본 인자값은 함수가 정의될 때 딱 한 번만 평가되어, 모든 호출이 같은 객체를 공유합니다. 리스트나 딕셔너리를 기본값으로 쓰면 호출 간에 상태가 새어 나갑니다.

def bad_append(item, target=[]):   # target이 모든 호출에서 공유됨
    target.append(item)
    return target

print(bad_append(1))
# 출력: [1]
print(bad_append(2))
# 출력: [1, 2]

두 번째 호출에서 [2]가 아니라 [1, 2]가 나옵니다. 빈 리스트가 매번 새로 생기는 게 아니기 때문입니다. JS의 function f(x, t = [])는 호출마다 새 배열을 만들지만, Python은 그렇지 않습니다.

⚠️ JS 함정: 가변 객체(list·dict·set)를 기본 인자로 절대 쓰지 마세요. JS 습관대로 def f(x=[])라고 쓰면 상태가 호출 간에 공유되어 디버깅이 극도로 어려운 버그가 됩니다.

해결책은 기본값을 None으로 두고 함수 안에서 새로 만드는 관용구입니다. 이 패턴을 손에 익히세요.

def good_append(item, target=None):
    if target is None:
        target = []
    target.append(item)
    return target

print(good_append(1))
# 출력: [1]
print(good_append(2))
# 출력: [2]

이제 호출마다 독립된 리스트가 생깁니다. is None으로 검사하는 이유는 2장에서 다룬 정체성 비교 관용구입니다.

연습문제

Q1. 임의 개수의 숫자를 받아 평균을 반환하는 함수입니다. 빈칸을 채우세요.

def average(____nums):
    return sum(nums) / len(nums)

print(average(10, 20, 30))   # 20.0
정답
def average(*nums):
    return sum(nums) / len(nums)

print(average(10, 20, 30))
# 출력: 20.0

*nums가 위치 인자를 튜플로 모읍니다.

Q2. 가변 기본값 함정을 피하도록 빈칸을 채우세요.

def collect(value, bucket=____):
    if bucket is None:
        bucket = []
    bucket.append(value)
    return bucket
정답
def collect(value, bucket=None):
    if bucket is None:
        bucket = []
    bucket.append(value)
    return bucket

print(collect(1))
# 출력: [1]

기본값을 None으로 두고 함수 내부에서 새 리스트를 생성해 호출 간 공유를 막습니다.

Q3. 딕셔너리 opts를 키워드 인자로 펼쳐 connect에 전달하는 코드입니다. 빈칸을 채우세요.

def connect(host, port=5432):
    return f"{host}:{port}"

opts = {"host": "srv", "port": 8080}
print(connect(____opts))   # srv:8080
정답
def connect(host, port=5432):
    return f"{host}:{port}"

opts = {"host": "srv", "port": 8080}
print(connect(**opts))
# 출력: srv:8080

**가 딕셔너리를 키워드 인자로 펼칩니다.

이 챕터의 JS→Python 대응 한눈에 보기

JSPython비고
(a, b) => a + bdef f(a, b): return a + b암묵 return 없음
x => x * xlambda x: x * x본문은 단일 표현식만
기본 매개변수 b = 10b=10가변 객체 금지
rest ...args*args(튜플)위치 인자 모으기
(이름 인자 모으기 없음)**kwargs(dict)키워드 인자 모으기
spread 호출 f(...arr)f(*arr) / f(**d)언패킹 호출
(함정 없음)def f(x=[]) 공유None 기본값 관용구 필수

Chapter 08. 스코프와 클로저

학습 목표
- Python이 함수 단위 스코프(LEGB)를 쓰며 if/for 블록은 스코프를 만들지 않음을 이해한다
- global/nonlocal로 바깥 스코프 변수를 재할당할 수 있다
- 클로저를 만들어 상태를 캡처하고, JS와 같은 렉시컬 캡처 원리를 확인한다
- 루프 안 클로저의 늦은 바인딩 함정을 lambda x=x: 트릭으로 회피할 수 있다

8.1 스코프의 단위 — 블록이 아니라 함수

JS는 let/const 도입 후 블록 스코프를 씁니다. { } 안에서 선언한 변수는 그 블록을 벗어나면 사라집니다. Python은 다릅니다. 스코프의 단위는 함수이며, if·for·while 블록은 새 스코프를 만들지 않습니다.

JavaScriptPython
``if (true) { let x = 1; }`<br>`// x는 여기서 접근 불가````if True:`<br>` x = 1`<br>`# x는 여기서 접근 가능``

아래 코드에서 for 블록 안에서 만든 변수가 루프 밖에서도 살아 있습니다. JS라면 블록 스코프 변수가 새어 나오지 않지만, Python에서는 정상입니다.

for i in range(3):
    last = i

print(last)
# 출력: 2
print(i)
# 출력: 2

Python의 스코프 탐색 규칙을 LEGB라고 부릅니다. 이름을 찾을 때 Local(현재 함수) → Enclosing(감싸는 함수) → Global(모듈) → Built-in(내장) 순으로 올라갑니다. JS의 렉시컬 스코프 체인과 개념이 같되, 계층 이름이 정해져 있다고 보면 됩니다.

x = "global"

def outer():
    x = "enclosing"
    def inner():
        print(x)   # Local에 없으니 Enclosing에서 찾음
    inner()

outer()
# 출력: enclosing
🎯 AICE: 실기는 Jupyter 노트북에서 진행되며, 모든 셀이 하나의 전역(Global) 네임스페이스를 공유합니다. 앞 셀에서 만든 df, model 같은 변수가 뒤 셀에 그대로 남아 있어 편리하지만, 셀을 위아래로 오가며 실행하면 이전 값이 살아 있어 엉뚱한 결과가 나오는 실수가 잦습니다. 채점 전에는 반드시 Kernel → Restart & Run All로 위에서 아래로 한 번에 다시 실행해, 전역 네임스페이스를 깨끗한 상태에서 재구성했는지 확인하세요.

8.2 읽기는 되지만 재할당은 막힌다 — global과 nonlocal

여기서 JS 개발자가 놀라는 지점이 나옵니다. 바깥 스코프 변수를 읽는 것은 자유지만, 함수 안에서 그 변수에 값을 재할당하면 Python은 그것을 새로운 지역 변수로 간주합니다.

count = 0

def increment():
    # count = count + 1  # 이렇게 하면 UnboundLocalError
    return count + 1     # 읽기만 하면 문제없음

print(increment())
# 출력: 1

바깥 변수를 진짜로 재할당하려면 의도를 명시해야 합니다. 모듈 전역 변수는 global, 감싸는 함수의 변수는 nonlocal을 선언합니다.

count = 0

def increment():
    global count
    count += 1

increment()
increment()
print(count)
# 출력: 2
def make_counter():
    n = 0
    def step():
        nonlocal n
        n += 1
        return n
    return step

counter = make_counter()
print(counter())
# 출력: 1
print(counter())
# 출력: 2
⚠️ JS 함정: JS는 count++로 바깥 변수를 그냥 수정합니다. Python에서 함수 안 count += 1은 global/nonlocal 선언이 없으면 "지역 변수 count를 읽어서 더한다"로 해석되어 UnboundLocalError가 납니다. 재할당 의도가 있으면 반드시 선언하세요. 단, 리스트에 .append()처럼 객체를 변형하는 것은 재할당이 아니므로 선언이 필요 없습니다.

8.3 클로저 — 렉시컬 캡처는 JS와 같다

make_counter가 이미 클로저입니다. 안쪽 함수가 바깥 함수의 지역 변수를 기억한 채 반환되는 구조로, JS의 클로저와 원리가 동일합니다. 함수 팩토리 패턴도 그대로 옮겨집니다.

def multiplier(factor):
    def multiply(x):
        return x * factor
    return multiply

double = multiplier(2)
triple = multiplier(3)
print(double(10), triple(10))
# 출력: 20 30

각 클로저가 자기만의 factor를 캡처합니다. 여기까지는 JS 개발자에게 익숙한 그림입니다.

8.4 루프 클로저 늦은 바인딩 함정

JS var 시절의 악명 높은 함정이 Python에 그대로 존재합니다. 반복문에서 클로저를 만들면, 클로저는 루프 변수의 값이 아니라 변수 자체를 캡처합니다. 루프가 끝난 뒤 함수를 호출하면 모두 마지막 값을 봅니다.

funcs = []
for i in range(3):
    funcs.append(lambda: i)

print([f() for f in funcs])
# 출력: [2, 2, 2]

[0, 1, 2]를 기대했지만 [2, 2, 2]가 나옵니다. 세 클로저가 모두 같은 i를 참조하고, 루프가 끝났을 때 i는 2이기 때문입니다. JS의 for (var i ...) 함정과 완전히 같은 현상입니다.

⚠️ JS 함정: JS는 let으로 루프마다 새 바인딩을 만들어 이 문제를 해결했습니다. Python에는 그런 마법이 없습니다. 값을 그 시점에 고정하려면 기본 인자를 이용하는 관용구 lambda i=i:를 씁니다 — 기본값은 정의 시점에 평가되므로 현재 값이 즉시 캡처됩니다.
funcs = []
for i in range(3):
    funcs.append(lambda i=i: i)   # 기본 인자로 현재 값을 고정

print([f() for f in funcs])
# 출력: [0, 1, 2]

lambda i=i:에서 오른쪽 i는 루프의 현재 값으로 즉시 평가되어 기본값으로 박제됩니다. 7장의 "기본값은 정의 시점에 한 번 평가된다" 규칙이 여기서는 오히려 해결책이 됩니다.

연습문제

Q1. 함수 안에서 전역 변수 total을 누적하려 합니다. 빈칸을 채우세요.

total = 0

def add(n):
    ____ total
    total += n

add(5)
add(3)
print(total)   # 8
정답
total = 0

def add(n):
    global total
    total += n

add(5)
add(3)
print(total)
# 출력: 8

전역 변수를 재할당하려면 global 선언이 필요합니다.

Q2. 루프에서 만든 클로저가 각자 다른 값을 반환하도록 빈칸을 채우세요.

adders = []
for n in range(3):
    adders.append(lambda x, ____: x + n)

print(adders[0](10), adders[1](10), adders[2](10))   # 10 11 12
정답
adders = []
for n in range(3):
    adders.append(lambda x, n=n: x + n)

print(adders[0](10), adders[1](10), adders[2](10))
# 출력: 10 11 12

n=n 기본 인자가 루프의 현재 값을 즉시 캡처해 늦은 바인딩을 막습니다.

이 챕터의 JS→Python 대응 한눈에 보기

JSPython비고
블록 스코프(let/const)함수 스코프(LEGB)if/for는 스코프 아님
렉시컬 스코프 체인LEGB 탐색 순서개념 동일
바깥 변수 그냥 수정global/nonlocal 선언재할당엔 선언 필수
클로저(함수 팩토리)클로저(동일 원리)렉시컬 캡처
let이 루프마다 새 바인딩늦은 바인딩 함정 존재lambda x=x: 트릭

Chapter 09. 클래스와 객체

학습 목표
- class/__init__/self로 객체를 정의하고 JS 클래스 문법과의 차이를 안다
- 인스턴스 속성과 클래스 속성을 구분해 공유 상태 함정을 피한다
- dunder 메서드(__repr__, __len__)로 연산자·내장 함수 동작을 커스터마이즈한다
- 상속과 super()를 사용해 sklearn/keras 객체 코드를 읽을 토대를 만든다

9.1 클래스 정의 — self는 명시적이다

JS의 class 문법과 Python의 class는 겉모습이 비슷합니다. 결정적 차이는 하나입니다. Python은 모든 메서드의 첫 인자로 self를 명시합니다. JS의 this가 암묵적으로 바인딩되는 것과 달리, Python은 인스턴스를 직접 받습니다.

JavaScriptPython
``class User {`<br>` constructor(name) { this.name = name; }`<br>`}````class User:`<br>` def __init__(self, name):`<br>` self.name = name``

생성자는 constructor가 아니라 __init__이라는 dunder(double underscore) 메서드입니다. 인스턴스를 만들 때 new 키워드 없이 클래스를 함수처럼 호출합니다.

class User:
    def __init__(self, name):
        self.name = name

    def greet(self):
        return f"안녕하세요, {self.name}님"

u = User("Ada")
print(u.greet())
# 출력: 안녕하세요, Ada님
⚠️ JS 함정: 메서드 정의에서 self를 빠뜨리는 것이 Python 입문자의 최다 에러입니다. JS 습관대로 def greet():라고 쓰면 호출 시 TypeError: greet() takes 0 positional arguments but 1 was given이 납니다. 인스턴스 메서드의 첫 인자는 항상 self입니다. 호출할 때는 u.greet()처럼 인자를 안 넘겨도, Python이 자동으로 u를 self로 전달합니다.

9.2 인스턴스 속성과 클래스 속성

self.name처럼 self에 붙이면 인스턴스 속성으로 객체마다 독립적입니다. 반면 클래스 본문에 직접 쓴 변수는 클래스 속성으로 모든 인스턴스가 공유합니다. JS의 static 필드와 비슷하되, 인스턴스에서도 읽힌다는 점이 다릅니다.

class Circle:
    pi = 3.14159            # 클래스 속성 (공유)

    def __init__(self, r):
        self.r = r          # 인스턴스 속성 (개별)

    def area(self):
        return Circle.pi * self.r ** 2

c = Circle(10)
print(c.area())
# 출력: 314.159
print(Circle.pi)
# 출력: 3.14159
⚠️ JS 함정: 클래스 속성에 가변 객체(list·dict)를 쓰면 7장의 공유 함정이 재현됩니다. 모든 인스턴스가 같은 리스트를 공유하므로, 인스턴스별 컬렉션은 반드시 __init__ 안에서 self.items = []로 만드세요.
class Cart:
    def __init__(self):
        self.items = []     # 인스턴스마다 새 리스트

a, b = Cart(), Cart()
a.items.append("apple")
print(b.items)
# 출력: []

9.3 dunder 메서드 — 연산자와 내장 함수 커스터마이즈

dunder 메서드는 파이썬이 특정 문법·내장 함수를 만났을 때 자동으로 호출하는 특수 메서드입니다. JS의 toString(), Symbol.iterator, valueOf가 하던 일을 Python은 이름이 정해진 dunder로 처리합니다.

__repr__은 객체를 문자열로 표현하는 방법을 정의합니다. JS toString()의 대응이며, 디버깅 출력과 REPL 표시에 쓰입니다.

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Point({self.x}, {self.y})"

    def __len__(self):
        return 2

p = Point(3, 4)
print(p)
# 출력: Point(3, 4)
print(len(p))
# 출력: 2

len(p)를 호출하면 Python이 p.__len__()을 대신 불러 줍니다. 이렇게 내장 함수와 연산자 동작을 객체에 위임하는 체계가 dunder입니다.

🎯 AICE: 시험에서 dunder를 직접 구현할 일은 드뭅니다. 그러나 sklearn 모델을 셀에서 실행하면 LogisticRegression(C=1.0, ...) 같은 __repr__ 출력이 뜨는데, 이것이 무엇인지 알아야 결과를 읽을 수 있습니다. 또 model.fit(...), model.predict(...)가 전부 self를 받는 인스턴스 메서드라는 사실이 estimator API 이해의 바탕입니다.

9.4 상속과 super()

상속 문법은 클래스 이름 뒤 괄호에 부모를 적습니다. 부모 메서드 호출은 super()로 하며, JS와 개념이 같습니다.

JavaScriptPython
``class Dog extends Animal {}````class Dog(Animal):``
``super(name);````super().__init__(name)``
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return "..."

class Dog(Animal):
    def speak(self):
        return f"{self.name}: 멍멍"

d = Dog("바둑이")
print(d.speak())
# 출력: 바둑이: 멍멍

자식이 부모의 __init__을 확장할 때는 super().__init__()으로 부모 초기화를 먼저 호출합니다.

class Puppy(Dog):
    def __init__(self, name, age):
        super().__init__(name)   # 부모의 name 초기화
        self.age = age

pup = Puppy("콩이", 1)
print(pup.speak(), pup.age)
# 출력: 콩이: 멍멍 1

이 구조가 keras에서 class MyModel(tf.keras.Model)처럼 프레임워크 클래스를 상속해 커스텀 모델을 만드는 패턴의 기초입니다.

연습문제

Q1. 인스턴스 메서드가 올바로 동작하도록 빈칸을 채우세요.

class Counter:
    def __init__(____):
        self.n = 0

    def increment(____):
        self.n += 1

c = Counter()
c.increment()
print(c.n)   # 1
정답
class Counter:
    def __init__(self):
        self.n = 0

    def increment(self):
        self.n += 1

c = Counter()
c.increment()
print(c.n)
# 출력: 1

모든 인스턴스 메서드의 첫 인자는 self입니다.

Q2. print(book)이 "Book: Python"을 출력하도록 빈칸을 채우세요.

class Book:
    def __init__(self, title):
        self.title = title

    def ____(self):
        return f"Book: {self.title}"

book = Book("Python")
print(book)   # Book: Python
정답
class Book:
    def __init__(self, title):
        self.title = title

    def __repr__(self):
        return f"Book: {self.title}"

book = Book("Python")
print(book)
# 출력: Book: Python

__repr__이 객체의 문자열 표현을 정의합니다.

이 챕터의 JS→Python 대응 한눈에 보기

JSPython비고
class C {}class C:문법 유사
constructor()__init__(self, ...)생성자 dunder
this (암묵)self (명시)첫 인자로 항상 명시
new C()C()new 없음
static field클래스 속성인스턴스에서도 읽힘
toString()__repr__문자열 표현
extends / super()(Parent) / super().__init__()상속

Chapter 10. 모듈과 패키지

학습 목표
- from ... import ...의 어순이 JS import와 반대임을 이해하고 자유롭게 쓴다
- import ... as로 별칭을 붙여 import pandas as pd 관례를 소화한다
- 표준 라이브러리 모듈(math, json, random)을 import해 활용한다
- if __name__ == "__main__" 관용구의 의미와 용도를 안다

10.1 import 어순 — from이 먼저다

JS의 named import는 import { x } from 'module'로, 가져올 이름을 먼저 쓰고 모듈을 나중에 씁니다. Python은 정확히 반대입니다. 모듈을 from으로 먼저 지정하고 이름을 import합니다.

JavaScriptPython
``import { sqrt } from 'math';````from math import sqrt``
``import * as math from 'math';````import math``

import math는 모듈 전체를 가져와 math.sqrt(...)처럼 접근하고, from math import sqrt는 특정 이름만 현재 네임스페이스로 끌어옵니다. JS의 namespace import(* as)와 named import의 구분과 대응합니다.

import math
from math import sqrt, pi

print(math.floor(3.7))
# 출력: 3
print(sqrt(16))
# 출력: 4.0
print(round(pi, 2))
# 출력: 3.14

여러 이름을 한 번에 가져올 때는 쉼표로 나열합니다. 어순만 뒤집으면 JS 지식이 그대로 통합니다.

10.2 별칭 — import ... as

JS의 import { x as y }처럼 Python도 as로 별칭을 붙입니다. 데이터 분석 생태계에는 사실상 표준이 된 별칭 관례가 있습니다. 이 관례를 손가락에 익혀 두어야 남의 코드도 읽고 시험 답안도 씁니다.

JavaScriptPython
``import { x as y } from 'm';````from m import x as y``
``import * as np from 'numpy';````import numpy as np``
import json as j

data = j.dumps({"name": "Ada", "age": 36})
print(data)
# 출력: {"name": "Ada", "age": 36}
🎯 AICE: 실기 첫 셀은 거의 항상 import numpy as np, import pandas as pd, import matplotlib.pyplot as plt로 시작합니다. 이후 모든 코드가 pd.read_csv(...), np.array(...) 형태이므로 별칭을 틀리면 연쇄적으로 답이 무너집니다. pd, np, plt는 통째로 암기하세요.

10.3 표준 라이브러리 — 배터리 포함

Python은 "batteries included" 철학으로 방대한 표준 라이브러리를 기본 제공합니다. Node의 fs, path처럼 별도 설치 없이 import만 하면 됩니다. random으로 난수 예제를 볼 때는 시드를 고정해 재현성을 확보합니다.

import random

random.seed(42)          # 시드 고정 — 항상 같은 결과
print(random.randint(1, 100))
# 출력: 82
print(random.choice(["a", "b", "c"]))
# 출력: a
from datetime import date

d = date(2026, 7, 19)
print(d.year, d.month, d.day)
# 출력: 2026 7 19
print(d.isoformat())
# 출력: 2026-07-19

json, math, random, datetime, collections 등은 어떤 환경에서도 쓸 수 있으니 데이터 처리 전 단계에서 유용합니다.

10.4 if __name__ == "__main__"

이 관용구는 JS에 직접 대응이 없어 처음엔 낯섭니다. Python 파일은 두 가지로 쓰입니다. (1) python script.py로 직접 실행하거나, (2) 다른 파일에서 import되는 모듈로 쓰입니다. 파일이 직접 실행되면 그 파일의 __name__이 "__main__"이 되고, import되면 모듈 이름이 됩니다.

JavaScript (Node)Python
``if (require.main === module) {}````if __name__ == "__main__":``

따라서 아래 관용구는 "이 파일이 직접 실행될 때만 이 블록을 돌려라"라는 뜻입니다. 함수·클래스 정의는 import해서 재사용하되, 실행 진입점 코드는 이 가드 안에 넣어 import 시 실행되지 않게 막는 것이 목적입니다.

def main():
    print("직접 실행됨")

if __name__ == "__main__":
    main()
# 출력: 직접 실행됨

이 파일을 다른 곳에서 import하면 __name__이 파일명이 되므로 main()이 호출되지 않습니다. 라이브러리로도, 스크립트로도 쓸 수 있는 재사용 가능한 파일을 만드는 표준 패턴입니다.

연습문제

Q1. math에서 sqrt만 가져와 사용하도록 빈칸을 채우세요.

____ math ____ sqrt
print(sqrt(81))   # 9.0
정답
from math import sqrt
print(sqrt(81))
# 출력: 9.0

from 모듈 import 이름 어순으로 특정 이름만 가져옵니다.

Q2. pandas를 pd 별칭으로 가져오는 관례 코드입니다. 빈칸을 채우세요. (실행 대상 아님)

import pandas ____ pd
정답
import pandas as pd

as로 별칭을 붙입니다. pd는 데이터 분석의 사실상 표준 별칭입니다.

Q3. 파일이 직접 실행될 때만 run()을 호출하도록 빈칸을 채우세요.

def run():
    print("start")

if __name__ == "____":
    run()
정답
def run():
    print("start")

if __name__ == "__main__":
    run()
# 출력: start

직접 실행 시 __name__은 "__main__"이 됩니다.

이 챕터의 JS→Python 대응 한눈에 보기

JSPython비고
import { x } from 'm'from m import x어순 반대
import * as m from 'm'import m모듈 전체
import { x as y }from m import x as y별칭
(관례)import pandas as pd표준 별칭 암기
require.main === moduleif __name__ == "__main__"실행 진입점 가드

Chapter 11. 에러 처리

학습 목표
- try/except/else/finally 구조를 JS try/catch/finally와 대응해 이해한다
- except에 예외 타입을 명시해 필요한 예외만 잡는다
- raise로 예외를 직접 발생시키고 예외 계층 구조를 활용한다
- else 절의 용도를 이해해 "성공했을 때만" 실행할 코드를 분리한다

11.1 try/except — catch가 except다

JS의 try/catch는 Python에서 try/except가 됩니다. 키워드 이름만 다르고 흐름은 같습니다. 다만 Python은 잡을 예외 타입을 except 절에 명시하는 것을 권장합니다.

JavaScriptPython
``try { risky(); }`<br>`catch (e) { handle(e); }````try:`<br>` risky()`<br>`except Exception as e:`<br>` handle(e)``

아래는 리스트 인덱스 초과를 잡는 예입니다. except IndexError처럼 타입을 지정하면, 그 종류의 예외만 잡고 나머지는 그대로 위로 전파됩니다.

nums = [10, 20, 30]

try:
    print(nums[5])
except IndexError as e:
    print(f"인덱스 에러: {e}")
# 출력: 인덱스 에러: list index out of range

여러 예외를 각각 다르게 처리하려면 except 절을 여러 개 둡니다. 위에서 아래로 처음 매칭되는 절이 실행됩니다.

def safe_divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        return "0으로 나눌 수 없습니다"
    except TypeError:
        return "숫자만 가능합니다"

print(safe_divide(10, 0))
# 출력: 0으로 나눌 수 없습니다
print(safe_divide(10, "x"))
# 출력: 숫자만 가능합니다
⚠️ JS 함정: JS의 catch (e)는 모든 에러를 무차별로 잡습니다. Python에서 except:를 타입 없이 쓰면 KeyboardInterrupt까지 삼켜 버려 프로그램을 멈출 수 없게 만듭니다. 반드시 except SomeError:처럼 타입을 명시하고, 정말 넓게 잡아야 하면 except Exception:을 쓰세요.

11.2 else와 finally

finally는 JS와 동일하게 예외 발생 여부와 무관하게 항상 실행됩니다. Python에는 여기에 더해 JS에 없는 else 절이 있습니다. else는 try 블록이 예외 없이 성공했을 때만 실행됩니다.

def read_value(data, key):
    try:
        value = data[key]
    except KeyError:
        print("키 없음")
        return None
    else:
        print("조회 성공")   # 예외가 없을 때만
        return value
    finally:
        print("조회 시도 종료")   # 항상

read_value({"a": 1}, "a")
# 출력: 조회 성공

else의 의미는 "try에서 감쌀 코드는 최소로 두고, 성공 후 이어질 로직은 else로 분리하라"입니다. 이렇게 하면 어느 줄이 예외를 낼 수 있는지가 명확해집니다.

11.3 raise — 예외 직접 발생

JS의 throw는 Python의 raise입니다. JS는 아무 값이나 던질 수 있지만, Python은 예외 클래스의 인스턴스를 발생시키는 것이 원칙입니다.

JavaScriptPython
``throw new Error("bad");````raise ValueError("bad")``

입력 검증에서 잘못된 값이 들어오면 적절한 예외를 발생시킵니다. ValueError는 "값은 맞는 타입이나 부적절할 때" 쓰는 표준 예외입니다.

def set_age(age):
    if age < 0:
        raise ValueError(f"나이는 음수일 수 없습니다: {age}")
    return age

try:
    set_age(-5)
except ValueError as e:
    print(e)
# 출력: 나이는 음수일 수 없습니다: -5

11.4 예외 계층 — 상속으로 묶인다

Python 예외는 클래스 계층을 이룹니다. ValueError, TypeError, IndexError 등은 모두 Exception을 상속합니다. 따라서 except Exception은 이들 대부분을 한꺼번에 잡습니다. 9장의 상속 지식이 여기서 실용적으로 쓰입니다.

errors = [ValueError("v"), IndexError("i"), KeyError("k")]
for e in errors:
    print(isinstance(e, Exception))
# 출력: True

계층을 이해하면 잡는 범위를 조절할 수 있습니다. 구체적인 타입(ZeroDivisionError)을 먼저 잡고, 넓은 타입(Exception)을 나중에 두는 것이 순서 원칙입니다 — 넓은 타입을 먼저 두면 구체 타입 절이 영원히 실행되지 않습니다.

def parse_int(s):
    try:
        return int(s)
    except ValueError:
        return -1

print(parse_int("42"))
# 출력: 42
print(parse_int("hello"))
# 출력: -1
🎯 AICE: 실기에서 예외 처리 자체를 크게 묻지는 않지만, 데이터 로딩·타입 변환 중 ValueError, KeyError가 자주 등장합니다. 어떤 예외가 왜 났는지 traceback 마지막 줄을 읽고 원인을 짚는 능력이 문제 해결 속도를 좌우합니다.

연습문제

Q1. 0으로 나누는 경우를 처리하도록 빈칸을 채우세요.

try:
    result = 10 / 0
____ ZeroDivisionError:
    result = None

print(result)   # None
정답
try:
    result = 10 / 0
except ZeroDivisionError:
    result = None

print(result)
# 출력: None

except 예외타입: 절로 특정 예외를 잡습니다.

Q2. 잘못된 입력에 예외를 발생시키도록 빈칸을 채우세요.

def sqrt_positive(x):
    if x < 0:
        ____ ValueError("음수 불가")
    return x ** 0.5

print(sqrt_positive(16))   # 4.0
정답
def sqrt_positive(x):
    if x < 0:
        raise ValueError("음수 불가")
    return x ** 0.5

print(sqrt_positive(16))
# 출력: 4.0

raise 예외(메시지)로 예외를 직접 발생시킵니다.

이 챕터의 JS→Python 대응 한눈에 보기

JSPython비고
try { } catch (e) { }try: ... except E as e:타입 명시 권장
catch (e) (전부 잡음)except Exception:무타입 except 지양
(없음)else: 절예외 없을 때만 실행
finally { }finally:항상 실행
throw new Error()raise ValueError()예외 인스턴스 발생
Error 상속Exception 계층isinstance로 확인

Chapter 12. 이터레이터와 제너레이터

학습 목표
- iterable과 iterator를 구분하고 iter()/next()의 동작을 이해한다
- yield로 제너레이터 함수를 만들어 값을 지연 생산한다
- 지연 평가(lazy evaluation)가 대용량 데이터 처리에 왜 유리한지 안다
- JS의 function*/Symbol.iterator와의 대응을 파악한다

12.1 iterable과 iterator — 순회의 두 역할

JS에서 for...of가 도는 대상은 Symbol.iterator를 가진 iterable입니다. Python도 같은 구조입니다. iterable은 iter()로 iterator를 만들 수 있는 객체(list·dict·문자열)이고, iterator는 next()로 값을 하나씩 꺼내는 객체입니다.

JavaScriptPython
``const it = arr[Symbol.iterator]();````it = iter(lst)``
``it.next().value````next(it)``

iter()로 iterator를 얻고 next()로 값을 하나씩 꺼냅니다. 값이 소진되면 StopIteration 예외가 납니다 — JS의 { done: true }에 해당합니다.

lst = [10, 20, 30]
it = iter(lst)

print(next(it))
# 출력: 10
print(next(it))
# 출력: 20
print(next(it))
# 출력: 30

for x in lst는 내부적으로 iter()를 호출하고 StopIteration이 날 때까지 next()를 반복하는 문법 설탕입니다. JS for...of와 동작 원리가 완전히 같습니다.

12.2 yield — 제너레이터 함수

yield가 들어간 함수는 일반 함수가 아니라 제너레이터 함수가 됩니다. JS의 function*와 정확히 대응합니다. 호출해도 본문이 즉시 실행되지 않고, 제너레이터 객체를 반환합니다. next()를 부를 때마다 다음 yield까지 실행되고 값을 내놓은 뒤 그 자리에서 멈춥니다.

JavaScriptPython
``function* gen() {`<br>` yield 1; yield 2;`<br>`}````def gen():`<br>` yield 1`<br>` yield 2``
def count_up_to(n):
    i = 1
    while i <= n:
        yield i
        i += 1

gen = count_up_to(3)
print(list(gen))
# 출력: [1, 2, 3]

제너레이터는 그 자체가 iterator이므로 for로 순회하거나 list()로 한꺼번에 모을 수 있습니다. 함수의 실행 상태(지역 변수 i)가 yield 사이에 그대로 보존된다는 점이 핵심입니다.

def fibonacci(count):
    a, b = 0, 1
    for _ in range(count):
        yield a
        a, b = b, a + b

print(list(fibonacci(7)))
# 출력: [0, 1, 1, 2, 3, 5, 8]

12.3 지연 평가 — 필요할 때 하나씩

제너레이터의 진짜 가치는 값을 미리 다 만들지 않는다는 데 있습니다. 리스트는 모든 원소를 메모리에 올리지만, 제너레이터는 요청받을 때마다 한 개씩 계산합니다. 백만 개를 다뤄도 메모리에는 한 번에 하나만 있습니다.

6장에서 본 제너레이터 표현식이 바로 이 지연 평가입니다. 대괄호 대신 소괄호를 쓰면 리스트가 아니라 제너레이터가 됩니다.

squares_list = [x ** 2 for x in range(5)]     # 즉시 다 계산
squares_gen = (x ** 2 for x in range(5))      # 지연 — 아직 계산 안 함

print(squares_list)
# 출력: [0, 1, 4, 9, 16]
print(squares_gen)
# 출력: <generator object
print(sum(squares_gen))
# 출력: 30

squares_gen을 출력하면 값이 아니라 제너레이터 객체가 보입니다. sum()이 순회하는 순간 비로소 값이 하나씩 만들어집니다.

⚠️ JS 함정: 제너레이터는 한 번만 순회할 수 있습니다. JS 배열은 몇 번이고 다시 돌 수 있지만, Python 제너레이터는 소진되면 끝입니다. 아래처럼 두 번째 순회는 빈 결과가 됩니다.
gen = (x for x in range(3))
print(list(gen))
# 출력: [0, 1, 2]
print(list(gen))   # 이미 소진됨
# 출력: []
🎯 AICE: Professional 트랙의 대용량 데이터 처리에서 제너레이터 사고가 바탕이 됩니다. pandas의 read_csv(..., chunksize=1000)은 DataFrame을 통째로 올리지 않고 청크 단위로 내주는 iterator를 반환하는데, 이것이 지연 평가의 실전 응용입니다. 메모리에 다 못 올리는 데이터를 조각내 처리하는 관점을 여기서 익혀 둡니다.

연습문제

Q1. iterator에서 값을 하나씩 꺼내도록 빈칸을 채우세요.

it = ____([100, 200])
print(____(it))   # 100
print(next(it))   # 200
정답
it = iter([100, 200])
print(next(it))
# 출력: 100
print(next(it))
# 출력: 200

iter()로 iterator를 만들고 next()로 값을 꺼냅니다.

Q2. 0부터 n까지 짝수를 지연 생산하는 제너레이터입니다. 빈칸을 채우세요.

def evens(n):
    for i in range(0, n + 1, 2):
        ____ i

print(list(evens(8)))   # [0, 2, 4, 6, 8]
정답
def evens(n):
    for i in range(0, n + 1, 2):
        yield i

print(list(evens(8)))
# 출력: [0, 2, 4, 6, 8]

yield가 함수를 제너레이터로 바꿔 값을 하나씩 내보냅니다.

Q3. 메모리를 아끼는 제너레이터 표현식으로 빈칸을 채우세요. (리스트가 아니어야 합니다)

big_sum = sum____x * 2 for x in range(1000)____
print(big_sum)   # 999000
정답
big_sum = sum(x * 2 for x in range(1000))
print(big_sum)
# 출력: 999000

sum() 안에 소괄호 제너레이터 표현식을 넣으면 값을 하나씩 만들어 더해 메모리를 절약합니다.

이 챕터의 JS→Python 대응 한눈에 보기

JSPython비고
arr[Symbol.iterator]()iter(lst)iterator 생성
it.next().valuenext(it)다음 값 꺼내기
{ done: true }StopIteration소진 신호
function* () { yield }def f(): yield제너레이터 함수
(배열은 재순회 가능)제너레이터 1회 소진다시 못 돎
(없음 — 즉시 평가)지연 평가 (x for x in ...)대용량 데이터

Chapter 13. NumPy 기초

학습 목표
- ndarray를 생성하고 dtype·shape로 배열의 구조를 파악할 수 있다
- 인덱싱·슬라이싱과 불리언 마스크로 원하는 원소를 선택할 수 있다
- for 루프 없이 벡터화 연산과 브로드캐스팅으로 배열을 계산할 수 있다
- axis 인자로 행·열 방향 집계를 구분해 수행할 수 있다

JS에는 대응이 없는 첫 개념입니다. Array는 있지만 "숫자 n차원 배열을 한 번에 계산하는" 도구는 없습니다(TypedArray가 가장 가깝지만 벡터 연산은 없습니다). NumPy의 ndarray는 pandas와 scikit-learn이 딛고 선 토대이고, AICE 실기의 모든 데이터가 결국 이 배열 위에서 움직입니다. 이 챕터에서 얻어야 할 단 하나는 "루프 대신 배열 통째로 계산한다"는 사고 전환입니다.

13.1 ndarray 생성 — 리스트를 배열로

Python 리스트는 벡터 연산을 못 합니다. [1,2,3] * 2는 원소가 2배가 아니라 리스트가 2번 반복됩니다(JS의 배열과 같은 함정). 계산이 목적이면 np.array로 감쌉니다.

JavaScriptPython (NumPy)
const a = new Float64Array([1,2,3]);a = np.array([1, 2, 3])
a.lengtha.shape
import numpy as np

nums = [1, 2, 3]
print(nums * 2)          # 리스트: 반복
# 출력: [1, 2, 3, 1, 2, 3]

arr = np.array([1, 2, 3])
print(arr * 2)           # 배열: 원소별 2배
# 출력: [2 4 6]

자주 쓰는 생성 함수들입니다. AICE 실기에서 초기 배열·가중치·더미 데이터를 만들 때 등장합니다.

z = np.zeros(3)                 # [0. 0. 0.]
o = np.ones((2, 3))             # 2행 3열 1로 채움
r = np.arange(0, 10, 2)         # range의 배열판
print(r)
# 출력: [0 2 4 6 8]

lin = np.linspace(0, 1, 5)      # 0~1 균등 5개
print(lin)
# 출력: [0.   0.25 0.5  0.75 1.  ]
⚠️ 함정: arr 2가 원소별 연산인 것은 NumPy 배열일 때만입니다. 순수 Python 리스트에 2를 쓰면 JS Array.prototype.concat처럼 이어붙이기가 됩니다. 벡터 연산이 필요하면 먼저 np.array로 변환하세요.

13.2 dtype와 shape — 배열의 신분증

배열은 리스트와 달리 원소 타입이 하나로 고정됩니다. dtype이 그 타입, shape가 차원 구조입니다.

mat = np.array([[1, 2, 3],
                [4, 5, 6]])
print(mat.shape)     # (행, 열)
# 출력: (2, 3)
print(mat.ndim)      # 차원 수
# 출력: 2
print(mat.dtype)     # 원소 타입
# 출력: int64

정수 배열에 실수를 섞으면 전체가 float64로 올라갑니다. 타입을 바꾸려면 astype을 씁니다.

f = np.array([1, 2, 3], dtype="float64")
print(f)
# 출력: [1. 2. 3.]

i = f.astype("int64")
print(i.dtype)
# 출력: int64

reshape로 shape를 바꿉니다. 원소 수만 맞으면 됩니다. -1은 "나머지 자동 계산"입니다.

a = np.arange(6)
print(a.reshape(2, 3))
# 출력: [[0 1 2]
print(a.reshape(3, -1).shape)   # -1 = 자동
# 출력: (3, 2)
🎯 AICE: 모델에 데이터를 넣기 전 X.shape를 찍어 (샘플 수, 특성 수)를 확인하는 것은 실기의 기본 습관입니다. shape가 맞지 않아 나는 에러가 가장 흔하므로, 배열을 만들 때마다 shape를 눈으로 확인하세요.

13.3 인덱싱과 슬라이싱 — 리스트 문법의 확장

1차원 인덱싱·슬라이싱은 4장에서 배운 리스트와 똑같습니다. 새로운 것은 2차원을 쉼표로 접근하는 문법입니다. JS에서 mat[i][j]로 쓰던 것을 NumPy는 mat[i, j] 한 쌍으로 씁니다.

JavaScriptPython (NumPy)
mat[0][1]mat[0, 1]
mat.map(row => row[0])mat[:, 0] (0열 전체)
mat = np.array([[10, 20, 30],
                [40, 50, 60]])
print(mat[0, 1])       # 0행 1열
# 출력: 20
print(mat[:, 0])       # 모든 행의 0열
# 출력: [10 40]
print(mat[1, :])       # 1행 전체
# 출력: [40 50 60]
print(mat[:, 1:])      # 1열부터 끝까지
# 출력: [[20 30]

불리언 인덱싱은 pandas의 핵심 전환점으로 이어지는 가장 중요한 문법입니다. 조건식이 True/False 배열을 만들고, 그 마스크로 원소를 골라냅니다.

data = np.array([12, 5, 30, 8, 21])
mask = data > 10
print(mask)
# 출력: [ True False  True False  True]
print(data[data > 10])     # 마스크로 필터
# 출력: [12 30 21]

data[data > 10] = 0        # 조건부 대입도 가능
print(data)
# 출력: [ 0  5  0  8  0]
⚠️ 함정: 조건을 여러 개 결합할 때 and/or가 아니라 &/|를 쓰고, 각 조건을 괄호로 감싸야 합니다. (data > 10) & (data < 25)가 맞고, data > 10 and data < 25는 에러입니다. 연산자 우선순위 때문에 괄호를 빠뜨리면 조용히 틀립니다.
d = np.array([12, 5, 30, 8, 21])
print(d[(d > 10) & (d < 25)])
# 출력: [12 21]

13.4 벡터화 연산 — 루프를 지운다

이 챕터의 핵심입니다. JS라면 for나 map으로 돌 계산을, NumPy는 배열 통째로 한 줄에 씁니다. C 레벨에서 실행되므로 빠르고, 무엇보다 코드가 수식 그대로입니다.

JavaScript

const celsius = [0, 25, 100];
const fahrenheit = celsius.map(c => c * 9 / 5 + 32);
// [32, 77, 212]

Python (NumPy)

celsius = np.array([0, 25, 100])
fahrenheit = celsius * 9 / 5 + 32     # 루프 없음
print(fahrenheit)
# 출력: [ 32.  77. 212.]

배열끼리의 연산도 원소별로 정렬되어 계산됩니다. 내장 함수(np.sqrt, np.exp, np.log 등)도 전부 벡터화되어 있습니다.

a = np.array([1, 2, 3])
b = np.array([10, 20, 30])
print(a + b)
# 출력: [11 22 33]
print(np.sqrt(np.array([1, 4, 9])))
# 출력: [1. 2. 3.]
🎯 AICE: 파생 변수를 만들 때 df['bmi'] = df['weight'] / (df['height'] ** 2)처럼 열 전체를 한 번에 계산하는 것이 이 벡터화 사고입니다. pandas의 열(Series)이 내부적으로 NumPy 배열이라, 여기서 익힌 감각이 그대로 이어집니다.

13.5 브로드캐스팅 — 모양이 다른 배열의 연산

shape가 다른 배열끼리 연산할 때, NumPy는 작은 쪽을 자동으로 "펼쳐" 맞춥니다. 이것이 브로드캐스팅입니다. 가장 흔한 경우는 배열과 스칼라(위에서 * 2가 이미 브로드캐스팅입니다)이고, 그다음이 행렬의 각 행/열에 벡터를 더하는 경우입니다.

mat = np.array([[1, 2, 3],
                [4, 5, 6]])
col_mean = mat.mean(axis=0)     # 열별 평균: [2.5 3.5 4.5]
centered = mat - col_mean       # (2,3) - (3,) → 행마다 빼기
print(centered)
# 출력: [[-1.5 -1.5 -1.5]
#        [ 1.5  1.5  1.5]]

규칙은 "뒤 차원부터 맞춰보고, 크기가 같거나 한쪽이 1이면 늘린다"입니다. 표준화(각 열에서 평균을 빼고 표준편차로 나누기)가 정확히 이 패턴이며, 15장 스케일링의 밑그림입니다.

std = mat.std(axis=0)
standardized = (mat - col_mean) / std
print(standardized.mean(axis=0).round(1))   # 각 열 평균 0
# 출력: [0. 0. 0.]
⚠️ 함정: 브로드캐스팅은 shape가 규칙에 맞을 때만 동작합니다. (2,3) 배열에 크기 2인 벡터를 더하면 ValueError가 납니다. "한쪽이 1이거나 크기가 같아야 한다"를 기억하세요. 열 방향으로 맞추고 싶으면 (2,1) 모양으로 reshape해야 합니다.

13.6 축(axis) 집계 — 행이냐 열이냐

sum, mean, max 같은 집계 함수에 axis를 주면 방향이 정해집니다. axis=0은 행을 따라 아래로(→ 열별 결과), axis=1은 열을 따라 옆으로(→ 행별 결과)입니다. 이 방향 감각이 pandas groupby까지 이어집니다.

mat = np.array([[1, 2, 3],
                [4, 5, 6]])
print(mat.sum())            # 축 없음: 전체 합
# 출력: 21
print(mat.sum(axis=0))      # 열별 합 (세로로 접기)
# 출력: [5 7 9]
print(mat.sum(axis=1))      # 행별 합 (가로로 접기)
# 출력: [ 6 15]
🎯 AICE: "각 특성(열)의 평균"이 필요하면 axis=0, "각 샘플(행)의 합"이 필요하면 axis=1입니다. 헷갈릴 때는 "없어지는 축이 axis"라고 외우세요. axis=0을 주면 행 방향이 접혀 사라지고 열별 결과가 남습니다.

연습문제

Q1. 1부터 12까지의 정수를 3행 4열 배열로 만들려고 합니다. 빈칸을 채우세요.

import numpy as np
arr = np.arange(1, ____).____(3, 4)
print(arr.shape)   # (3, 4)
정답
import numpy as np
arr = np.arange(1, 13).reshape(3, 4)
print(arr.shape)
# 출력: (3, 4)

arange(1, 13)은 1~12(끝값 제외)로 12개를 만들고, reshape(3, 4)로 3×4에 담습니다.

Q2. 배열 scores에서 60점 이상인 값만 골라내려 합니다. 빈칸을 채우세요.

scores = np.array([50, 72, 88, 41, 65])
passed = scores[____ >= 60]
print(passed)   # [72 88 65]
정답
scores = np.array([50, 72, 88, 41, 65])
passed = scores[scores >= 60]
print(passed)
# 출력: [72 88 65]

불리언 마스크 scores >= 60이 True/False 배열을 만들고, 대괄호 안에 넣으면 True 위치만 남습니다.

Q3. 2차원 배열 mat의 열별 평균을 구하려 합니다. 빈칸을 채우세요.

mat = np.array([[1, 2], [3, 4], [5, 6]])
col_avg = mat.mean(axis=____)
print(col_avg)   # [3. 4.]
정답
mat = np.array([[1, 2], [3, 4], [5, 6]])
col_avg = mat.mean(axis=0)
print(col_avg)
# 출력: [3. 4.]

열별 결과를 원하므로 행 방향(axis=0)을 접습니다. "없어지는 축이 axis"를 기억하세요.

이 챕터의 JS→Python 대응 한눈에 보기

JSPython (NumPy)비고
new Float64Array([...])np.array([...])벡터 연산 가능한 배열
arr.lengtharr.shapeshape는 튜플(다차원)
mat[i][j]mat[i, j]쉼표로 다차원 접근
arr.map(f)arr 통째 벡터 연산루프·map 불필요
arr.filter(x => x > 10)arr[arr > 10]불리언 인덱싱
조건 결합 && / `\\`& / `\` (괄호 필수)and/or 아님
arr.reduce((a,b)=>a+b)arr.sum() / arr.sum(axis=0)axis로 방향 지정

Chapter 14. Pandas I: Series와 DataFrame

학습 목표
- Series와 DataFrame의 관계를 JS의 배열/객체 모델에 대응시켜 이해할 수 있다
- read_csv로 데이터를 로드하고 head/info/describe로 구조를 파악할 수 있다(EDA 1단계)
- loc/iloc로 라벨·정수 위치 기반 행·열 선택을 구분해 수행할 수 있다
- 불리언 인덱싱으로 조건에 맞는 행을 필터링할 수 있다

pandas는 AICE Associate 실기의 무대입니다. 시험은 "데이터를 읽고(EDA) → 다듬고(전처리) → 나누고 → 모델에 넣는" 흐름이며, 이 챕터는 그 첫 단계인 로드와 구조 확인을 다룹니다. JS 개발자에게 DataFrame은 "각 열이 같은 길이인 객체들의 배열"이라고 생각하면 가장 가깝습니다.

14.1 Series와 DataFrame — 열과 표

Series는 인덱스가 붙은 1차원 배열(13장 ndarray + 라벨), DataFrame은 Series를 열로 묶은 2차원 표입니다. JS로 치면 Series는 "이름표 달린 배열", DataFrame은 "행 객체들의 배열"입니다.

JavaScriptPython (pandas)
const ages = [25, 30, 35];ages = pd.Series([25, 30, 35])
[{name:'Kim', age:25}, ...]pd.DataFrame({"name": [...], "age": [...]})

DataFrame은 보통 열 단위 딕셔너리로 만듭니다. JS의 "행 객체 배열"과 축이 반대라는 점에 주의하세요.

import pandas as pd

df = pd.DataFrame({
    "name": ["Kim", "Lee", "Park", "Choi"],
    "age": [25, 30, 35, 28],
    "city": ["Seoul", "Busan", "Seoul", "Daegu"],
})
print(df.shape)      # (행, 열)
# 출력: (4, 3)
print(df["age"].mean())
# 출력: 29.5

하나의 열을 꺼내면 Series입니다. Series는 13장의 벡터 연산을 그대로 지원합니다.

print(type(df["age"]))
# 출력: <class 'pandas.Series'>
older = df["age"] + 1        # 열 전체에 벡터 연산
print(older.tolist())
# 출력: [26, 31, 36, 29]
⚠️ JS 함정: DataFrame은 "행 객체의 배열"이 아니라 "열의 딕셔너리"입니다. df[0]은 첫 번째 행이 아니라 이름이 0인 열을 찾으려다 에러가 납니다. 행을 위치로 꺼내려면 df.iloc[0]을 써야 합니다(14.3).

14.2 read_csv와 EDA 3종 세트 — head/info/describe

실기에서 데이터는 CSV로 주어집니다. pd.read_csv("파일경로")가 표준이지만, 여기서는 실행 가능하도록 문자열을 io.StringIO로 감싸 읽습니다. 실기에서는 파일 경로 문자열만 넣으면 됩니다.

import io

csv_text = """name,age,city,score
Kim,25,Seoul,88
Lee,30,Busan,72
Park,35,Seoul,95
Choi,28,Daegu,63
Yoon,42,Busan,80"""

df = pd.read_csv(io.StringIO(csv_text))
print(df.shape)
# 출력: (5, 4)

로드 직후 데이터의 첫인상을 보는 세 함수입니다. 순서대로 외워 두면 실기에서 그대로 씁니다.

head3 = df.head(3)          # 앞 3행 (기본 5행)
print(head3.shape)
# 출력: (3, 4)

info()는 행 수·열별 결측 여부·dtype을 한눈에 보여줍니다(결측치 파악의 출발점). 출력의 첫 줄은 항상 클래스 이름입니다.

df.info()
# 출력: <class 'pandas.DataFrame'>

describe()는 수치형 열의 요약 통계(개수·평균·표준편차·사분위수)를 냅니다.

desc = df.describe()
print(desc.loc["mean", "age"])   # age 열 평균
# 출력: 32.0
print(desc.loc["max", "score"])  # score 최댓값
# 출력: 95.0
🎯 AICE: 실기 EDA 문항은 대개 df.head(), df.info(), df.describe()를 순서대로 요구합니다. 특히 info()로 결측치가 있는 열을 찾아 다음 전처리 단계로 넘어가는 흐름이 정형화되어 있으니, 이 세 함수는 손에 익혀 두세요.

14.3 loc와 iloc — 라벨이냐 위치냐

행·열을 선택하는 두 가지 방식입니다. loc는 라벨(이름) 기반, iloc는 정수 위치 기반입니다. JS 배열은 정수 인덱스만 있지만, pandas는 인덱스에 이름을 붙일 수 있어 둘을 구분합니다.

JavaScriptPython (pandas)
rows[0]df.iloc[0] (위치)
(대응 없음)df.loc[0, "age"] (라벨)
df = pd.DataFrame({
    "name": ["Kim", "Lee", "Park"],
    "age": [25, 30, 35],
}, index=["a", "b", "c"])     # 문자열 인덱스

print(df.loc["b", "age"])     # 라벨로 접근
# 출력: 30
print(df.iloc[1, 1])          # 정수 위치로 접근
# 출력: 30

슬라이싱도 됩니다. 여러 열은 리스트로 넘깁니다. loc의 범위 슬라이싱은 끝값을 포함하는 점이 4장의 리스트 슬라이싱과 다릅니다.

print(df.loc["a":"b", "name"].tolist())   # 끝 라벨 포함
# 출력: ['Kim', 'Lee']
print(df.iloc[0:2]["name"].tolist())       # 끝 위치 제외(리스트와 동일)
# 출력: ['Kim', 'Lee']
sub = df[["name", "age"]]                    # 여러 열 선택
print(sub.shape)
# 출력: (3, 2)
⚠️ JS 함정: loc의 슬라이싱 df.loc["a":"c"]는 끝 라벨 "c"를 포함합니다. JS의 slice나 Python 리스트 슬라이싱이 끝을 제외하는 것과 반대이므로, 정수 인덱스에 loc을 쓸 때 특히 조심하세요. 위치 기반 iloc은 끝을 제외합니다.

14.4 불리언 인덱싱 — pandas의 핵심 전환점

이 챕터에서 가장 중요한 문법입니다. 13장의 NumPy 불리언 마스크가 그대로 DataFrame으로 올라옵니다. 조건식이 True/False Series를 만들고, df[마스크]가 True인 행만 남깁니다. 실기에서 "조건을 만족하는 데이터 추출"은 거의 매번 나옵니다.

JavaScript

const adults = people.filter(p => p.age >= 30);

Python (pandas)

df = pd.DataFrame({
    "name": ["Kim", "Lee", "Park", "Choi", "Yoon"],
    "age": [25, 30, 35, 28, 42],
    "city": ["Seoul", "Busan", "Seoul", "Daegu", "Busan"],
})

adults = df[df["age"] >= 30]        # 30세 이상 행만
print(adults["name"].tolist())
# 출력: ['Lee', 'Park', 'Yoon']

조건을 결합할 때는 NumPy와 똑같이 &/|를 쓰고 각 조건을 괄호로 감쌉니다.

seoul_young = df[(df["city"] == "Seoul") & (df["age"] < 30)]
print(seoul_young["name"].tolist())
# 출력: ['Kim']

busan = df[df["city"].isin(["Busan"])]   # 여러 값 매칭은 isin
print(len(busan))
# 출력: 2

특정 열의 값 분포는 value_counts()로 셉니다(EDA에서 범주형 파악의 필수 도구).

counts = df["city"].value_counts()
print(counts["Seoul"])
# 출력: 2
⚠️ 함정: 여기서도 and/or가 아니라 &/|입니다. 그리고 각 조건은 반드시 괄호로 감싸세요. df[df["city"] == "Seoul" & df["age"] < 30]은 연산자 우선순위 때문에 에러가 납니다. df[(...) & (...)] 형태를 습관화하세요.
🎯 AICE: df[df['컬럼'] 조건] 패턴은 실기 빈칸 채우기의 단골입니다. "특정 조건의 행을 추출해 개수를 세라"거나 "조건에 맞는 행의 평균을 구하라"는 문항이 반복되므로, 마스크 → 대괄호 필터 → 집계로 이어지는 손동작을 몸에 익히세요.

연습문제

Q1. DataFrame df에서 age 열의 평균을 구하려 합니다. 빈칸을 채우세요.

import pandas as pd
df = pd.DataFrame({"age": [20, 30, 40], "score": [70, 80, 90]})
avg = df[____].mean()
print(avg)   # 30.0
정답
import pandas as pd
df = pd.DataFrame({"age": [20, 30, 40], "score": [70, 80, 90]})
avg = df["age"].mean()
print(avg)
# 출력: 30.0

열 하나를 문자열 키로 꺼내면 Series가 되고, .mean()으로 평균을 구합니다.

Q2. score가 75 이상인 행만 골라내려 합니다. 빈칸을 채우세요.

df = pd.DataFrame({"name": ["A", "B", "C"], "score": [60, 80, 90]})
high = df[df[____] >= 75]
print(high["name"].tolist())   # ['B', 'C']
정답
df = pd.DataFrame({"name": ["A", "B", "C"], "score": [60, 80, 90]})
high = df[df["score"] >= 75]
print(high["name"].tolist())
# 출력: ['B', 'C']

df["score"] >= 75가 불리언 Series를 만들고, df[...]가 True인 행만 남깁니다.

Q3. 2번째 행(정수 위치)을 통째로 꺼내려 합니다. 라벨이 아니라 위치 기반이어야 합니다. 빈칸을 채우세요.

df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
row = df.____[1]
print(row["a"])   # 2
정답
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
row = df.iloc[1]
print(row["a"])
# 출력: 2

정수 위치 기반 접근은 iloc입니다. 라벨 기반이라면 loc을 씁니다.

이 챕터의 JS→Python 대응 한눈에 보기

JSPython (pandas)비고
[{...}, {...}] (행 객체 배열)pd.DataFrame({열: [...]})열 딕셔너리로 생성
arr[i]df.iloc[i]정수 위치 접근
(대응 없음)df.loc[라벨, 열]라벨 기반 접근
arr.filter(p => p.x > 0)df[df["x"] > 0]불리언 인덱싱
arr.filter(p => A && B)df[(A) & (B)]괄호 필수, & 사용
arr.slice(0, 3)df.head(3)앞부분 미리보기
new Set(arr).size 계열df["col"].value_counts()값 빈도

Chapter 15. Pandas II: 전처리

학습 목표
- 결측치를 탐지하고 fillna/dropna로 처리할 수 있다
- 타입 변환과 파생 변수 생성으로 데이터를 가공할 수 있다
- groupby 집계와 merge 결합으로 데이터를 재구성할 수 있다
- 범주형 인코딩(get_dummies/LabelEncoder)과 스케일링으로 모델 입력을 완성할 수 있다

전처리는 AICE Associate 실기에서 가장 배점이 큰 영역입니다. "결측치를 채우고, 범주형을 숫자로 바꾸고, 스케일을 맞춰 모델에 넣을 수 있는 형태로 만드는" 과정 전체가 이 챕터입니다. JS에는 대응 개념이 거의 없으니, 각 함수의 시그니처를 손으로 쓸 수 있을 때까지 익히는 것이 목표입니다.

15.1 결측치 처리 — isnull / fillna / dropna

실제 데이터에는 빈 값(NaN)이 흔합니다. JS의 null/undefined에 해당하지만, pandas는 이를 열 단위로 한 번에 탐지·처리합니다. 먼저 어디에 얼마나 있는지 셉니다.

import pandas as pd
import numpy as np

df = pd.DataFrame({
    "name": ["Kim", "Lee", "Park", "Choi"],
    "age": [25, np.nan, 35, 28],
    "score": [88, 72, np.nan, 63],
})
print(df.isnull().sum().sum())   # 전체 결측 개수
# 출력: 2
print(df["age"].isnull().sum())  # age 열 결측 개수
# 출력: 1

fillna는 결측을 특정 값으로 채우고, dropna는 결측이 있는 행을 버립니다. pandas 3.0에서는 inplace=True 대신 결과를 다시 대입하는 방식이 권장됩니다.

df["age"] = df["age"].fillna(df["age"].mean())   # 평균으로 대치
print(df["age"].isnull().sum())
# 출력: 0

dropped = df.dropna()               # 결측 있는 행 제거
print(dropped.shape)                # score의 결측 행 하나 제거
# 출력: (3, 3)
⚠️ 함정: NaN은 자기 자신과도 같지 않습니다(JS와 동일). 따라서 df["age"] == np.nan으로는 절대 결측을 찾지 못하고 전부 False가 나옵니다. 결측 탐지는 반드시 isnull()(또는 isna())을 쓰세요.

15.2 타입 변환과 파생 변수

숫자가 문자열로 들어오거나, 기존 열을 조합해 새 열을 만들어야 할 때가 많습니다. 타입은 astype, 파생 변수는 벡터 연산으로 열을 통째 계산해 새 열에 대입합니다(13장 벡터화의 실전).

df = pd.DataFrame({
    "height": [170, 165, 180],
    "weight": [70, 55, 82],
    "grade": ["1", "2", "3"],       # 숫자인데 문자열
})
df["grade"] = df["grade"].astype("int64")   # 문자열 → 정수
print(df["grade"].dtype)
# 출력: int64

df["bmi"] = df["weight"] / (df["height"] / 100) ** 2   # 파생 변수
print(df["bmi"].round(1).tolist())
# 출력: [24.2, 20.2, 25.3]

조건에 따라 범주를 만드는 파생도 자주 씁니다. np.where가 JS의 삼항 연산을 벡터화한 것입니다.

df["obese"] = np.where(df["bmi"] >= 25, "yes", "no")
print(df["obese"].tolist())
# 출력: ['no', 'no', 'yes']

15.3 groupby — 범주별 집계

SQL의 GROUP BY와 같습니다. "도시별 평균 점수"처럼 범주별로 나눠 집계합니다. JS라면 reduce로 직접 누적해야 할 로직을 한 줄로 표현합니다.

JavaScript

const byCity = {};
for (const r of rows) (byCity[r.city] ??= []).push(r.score);
// 이후 각 배열의 평균을 다시 계산...

Python (pandas)

df = pd.DataFrame({
    "city": ["Seoul", "Busan", "Seoul", "Busan", "Seoul"],
    "score": [80, 70, 90, 60, 100],
})
by_city = df.groupby("city")["score"].mean()
print(by_city["Seoul"])
# 출력: 90.0
print(by_city["Busan"])
# 출력: 65.0

여러 집계를 한 번에 하려면 agg를 씁니다.

stats = df.groupby("city")["score"].agg(["mean", "count"])
print(stats.loc["Seoul", "count"])
# 출력: 3

15.4 merge — 두 표 결합하기

SQL JOIN에 해당합니다. 공통 키를 기준으로 두 DataFrame을 옆으로 붙입니다. on에 기준 열, how에 결합 방식(inner/left/right/outer)을 줍니다.

users = pd.DataFrame({"id": [1, 2, 3], "name": ["Kim", "Lee", "Park"]})
orders = pd.DataFrame({"id": [1, 1, 2], "amount": [100, 200, 150]})

merged = pd.merge(users, orders, on="id", how="inner")
print(merged.shape)          # id=3은 주문 없어 제외
# 출력: (3, 3)
print(merged["amount"].sum())
# 출력: 450
⚠️ 함정: how의 기본값은 "inner"라, 한쪽에만 있는 키는 조용히 사라집니다. 위 예에서 id=3인 Park은 주문이 없어 결과에서 빠집니다. 모든 행을 유지하려면 how="left"를 명시하세요. JS에서 수동 조인하던 감각으로 접근하면 누락을 놓치기 쉽습니다.

15.5 범주형 인코딩 — get_dummies / LabelEncoder

모델은 문자열을 못 먹습니다. 범주형을 숫자로 바꿔야 합니다. 두 방식이 있습니다. get_dummies는 원-핫 인코딩(범주마다 0/1 열 생성), LabelEncoder는 각 범주에 정수를 부여합니다.

df = pd.DataFrame({"city": ["Seoul", "Busan", "Seoul", "Daegu"]})
dummies = pd.get_dummies(df["city"])     # 원-핫: 범주 수만큼 열
print(dummies.shape)
# 출력: (4, 3)
print(dummies.columns.tolist())
# 출력: ['Busan', 'Daegu', 'Seoul']

LabelEncoder는 sklearn에서 가져오며, 순서가 있는 범주나 목표 변수(y)에 주로 씁니다.

from sklearn.preprocessing import LabelEncoder

le = LabelEncoder()
encoded = le.fit_transform(df["city"])   # 알파벳순 정수 부여
print(encoded.tolist())
# 출력: [2, 0, 2, 1]
print(le.classes_.tolist())
# 출력: ['Busan', 'Daegu', 'Seoul']
🎯 AICE: 입력 특성(X)의 범주형은 대개 get_dummies(원-핫), 목표 변수(y)의 범주 라벨은 LabelEncoder를 쓰는 것이 실기의 관례입니다. pd.get_dummies(df, columns=["열이름"])처럼 DataFrame 통째로 특정 열만 인코딩하는 형태가 빈칸으로 자주 나옵니다.

15.6 스케일링 — StandardScaler / MinMaxScaler

특성마다 단위와 범위가 다르면(나이 0~100, 소득 0~1억) 모델이 큰 값에 휘둘립니다. 스케일링으로 범위를 맞춥니다. 두 스케일러의 인터페이스가 동일(fit_transform)하므로 하나로 묶어 봅니다. StandardScaler는 평균 0·표준편차 1로, MinMaxScaler는 0~1 범위로 변환합니다.

from sklearn.preprocessing import StandardScaler, MinMaxScaler

X = pd.DataFrame({
    "age": [20, 40, 60, 80],
    "income": [2000, 4000, 6000, 8000],
})

std_scaled = StandardScaler().fit_transform(X)
print(std_scaled.mean(axis=0).round(1).tolist())   # 각 열 평균 0
# 출력: [0.0, 0.0]

minmax_scaled = MinMaxScaler().fit_transform(X)
print(minmax_scaled.min(axis=0).tolist())          # 각 열 최솟값 0
# 출력: [0.0, 0.0]
print(minmax_scaled.max(axis=0).tolist())          # 각 열 최댓값 1
# 출력: [1.0, 1.0]
⚠️ 함정: 스케일러는 학습 데이터에만 fit하고, 검증·테스트 데이터에는 transform만 해야 합니다. 전체 데이터에 fit_transform을 먼저 하면 테스트 정보가 새어 들어가는 데이터 누수가 생깁니다(21장에서 Pipeline으로 자동 방지). 실기에서는 분리(17장) 후 스케일링하는 순서를 지키세요.

연습문제

Q1. age 열의 결측치를 중앙값(median)으로 채우려 합니다. 빈칸을 채우세요.

import pandas as pd, numpy as np
df = pd.DataFrame({"age": [20, np.nan, 40, np.nan, 30]})
df["age"] = df["age"].____(df["age"].median())
print(df["age"].isnull().sum())   # 0
정답
import pandas as pd, numpy as np
df = pd.DataFrame({"age": [20, np.nan, 40, np.nan, 30]})
df["age"] = df["age"].fillna(df["age"].median())
print(df["age"].isnull().sum())
# 출력: 0

fillna에 채울 값을 넘깁니다. 결과를 열에 다시 대입해야 반영됩니다.

Q2. city 열을 원-핫 인코딩하려 합니다. 빈칸을 채우세요.

df = pd.DataFrame({"city": ["A", "B", "A", "C"]})
onehot = pd.____(df["city"])
print(onehot.shape)   # (4, 3)
정답
df = pd.DataFrame({"city": ["A", "B", "A", "C"]})
onehot = pd.get_dummies(df["city"])
print(onehot.shape)
# 출력: (4, 3)

pd.get_dummies가 범주마다 0/1 열을 만듭니다. 범주가 3종이라 열이 3개입니다.

Q3. 특성 X를 평균 0·표준편차 1로 표준화하려 합니다. 빈칸을 채우세요.

from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_scaled = scaler.____(X)
정답
from sklearn.preprocessing import StandardScaler
import pandas as pd
X = pd.DataFrame({"v": [1.0, 2.0, 3.0]})
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
print(round(float(X_scaled.mean()), 1))
# 출력: 0.0

fit_transform이 통계량 학습과 변환을 한 번에 수행합니다(학습 데이터에만 사용).

이 챕터의 JS→Python 대응 한눈에 보기

JSPython (pandas/sklearn)비고
x == null 체크df.isnull()== np.nan은 항상 False
x ?? 기본값df["c"].fillna(값)결측 대치
arr.filter(x => x != null)df.dropna()결측 행 제거
Number(x)df["c"].astype("int64")타입 변환
cond ? a : b (열 전체)np.where(cond, a, b)벡터화 삼항
reduce로 그룹 집계df.groupby("k")["v"].mean()SQL GROUP BY
수동 조인pd.merge(a, b, on="k")SQL JOIN
(대응 없음)get_dummies / LabelEncoder범주형 인코딩
(대응 없음)StandardScaler / MinMaxScaler스케일링

Chapter 16. 시각화

학습 목표
- matplotlib의 기본 플롯 구조(figure–plot–show)를 이해하고 그릴 수 있다
- seaborn의 countplot/histplot/boxplot으로 분포를 시각화할 수 있다
- 상관관계 heatmap으로 특성 간 관계를 읽을 수 있다
- 학습곡선을 그려 모델의 과적합 여부를 판단할 수 있다

시각화는 EDA에서 "숫자로는 안 보이는 것을 눈으로 확인하는" 단계입니다. JS 개발자라면 Chart.js나 D3로 그려 봤을 것입니다. Python 생태계에서는 matplotlib(저수준 엔진)과 그 위에 얹힌 seaborn(통계 플롯 특화)이 사실상 표준입니다. AICE 실기에서는 분포·상관·학습곡선을 그리는 문항이 반복됩니다.

📦 설치 안내: 이 챕터의 플롯을 로컬에서 실습하려면 다음 패키지가 필요합니다(기본 파이썬 환경에는 없을 수 있습니다).
```bash
pip install matplotlib seaborn
```
Jupyter에서는 %matplotlib inline을 한 번 실행하면 셀 아래에 그림이 바로 표시됩니다.

16.1 matplotlib 기본 — figure, plot, show

matplotlib의 정신 모델은 "빈 도화지(figure)에 요소를 쌓고 마지막에 보여준다(show)"입니다. Chart.js가 설정 객체를 넘기는 선언형이라면, matplotlib는 명령을 순서대로 쌓는 명령형에 가깝습니다.

JavaScript (Chart.js)Python (matplotlib)
new Chart(ctx, {type:'line', ...})plt.plot(x, y)
chart.options.titleplt.title("...")
import matplotlib.pyplot as plt

x = [1, 2, 3, 4, 5]
y = [1, 4, 9, 16, 25]

plt.figure(figsize=(6, 4))     # 도화지 크기(가로, 세로 인치)
plt.plot(x, y, marker="o")     # 선 + 점
plt.title("y = x^2")
plt.xlabel("x")
plt.ylabel("y")
plt.show()                     # 화면에 표시

여러 그래프를 나란히 놓을 때는 subplots를 씁니다. 반환된 축(ax) 객체에 그립니다.

fig, axes = plt.subplots(1, 2, figsize=(10, 4))   # 1행 2열
axes[0].plot(x, y)
axes[0].set_title("Line")
axes[1].scatter(x, y)          # 산점도
axes[1].set_title("Scatter")
plt.tight_layout()             # 겹침 방지
plt.show()
⚠️ 함정: matplotlib는 "현재 활성 figure"라는 전역 상태를 씁니다. plt.plot을 연달아 호출하면 같은 그림에 겹쳐 그려집니다. 새 그림을 원하면 plt.figure()로 새 도화지를 열거나, 위처럼 subplots로 축을 명시적으로 다루세요. JS의 캔버스별 독립 컨텍스트와 달리 상태가 공유됩니다.

16.2 seaborn — 분포를 한 줄로

seaborn은 DataFrame을 직접 받아 통계 플롯을 그립니다. data=에 DataFrame, x=/y=에 열 이름 문자열을 넘기는 패턴이 일관됩니다. 실기 빈출 3종을 봅니다.

countplot은 범주형 열의 빈도를 셉니다(막대그래프). df["col"].value_counts()를 그림으로 보는 것입니다.

import seaborn as sns
import pandas as pd

df = pd.DataFrame({
    "city": ["Seoul", "Busan", "Seoul", "Daegu", "Seoul", "Busan"],
    "age": [25, 30, 35, 28, 42, 33],
})
sns.countplot(data=df, x="city")   # 도시별 개수
plt.title("City Counts")
plt.show()

histplot은 수치형 열의 분포를 구간(bin)으로 나눠 봅니다. kde=True를 주면 밀도 곡선을 겹칩니다.

sns.histplot(data=df, x="age", bins=10, kde=True)
plt.title("Age Distribution")
plt.show()

boxplot은 사분위수·이상치를 한눈에 보여줍니다. 범주별 분포 비교에 유용합니다(x에 범주, y에 수치).

sns.boxplot(data=df, x="city", y="age")   # 도시별 나이 분포
plt.title("Age by City")
plt.show()
🎯 AICE: countplot(범주 빈도), histplot(수치 분포), boxplot(이상치 확인)은 실기 EDA 문항에서 "분포를 시각화하라"는 지시에 그대로 대응합니다. sns.countplot(data=df, x='컬럼')처럼 data와 x 인자를 빈칸으로 두는 형태가 자주 출제되니 인자 이름을 정확히 외우세요.

16.3 상관관계 heatmap

수치형 특성들이 서로 얼마나 함께 움직이는지를 봅니다. df.corr()가 상관계수 행렬(−1~1)을 만들고, sns.heatmap이 색으로 표현합니다. 상관이 높은 특성 쌍을 찾아 다중공선성을 점검하는 EDA 단골입니다.

num_df = pd.DataFrame({
    "age": [25, 30, 35, 40, 45],
    "income": [2000, 2500, 4000, 5000, 7000],
    "score": [80, 70, 75, 60, 55],
})
corr = num_df.corr()               # 상관계수 행렬
sns.heatmap(corr, annot=True, cmap="coolwarm", vmin=-1, vmax=1)
plt.title("Correlation Heatmap")
plt.show()

annot=True는 셀에 숫자를 표시하고, cmap은 색 팔레트, vmin/vmax로 색 범위를 −1~1에 고정합니다.

⚠️ 함정: df.corr()는 수치형 열만 대상으로 계산합니다. 문자열 열이 섞여 있으면 자동으로 제외되거나 버전에 따라 에러가 날 수 있으니, df.select_dtypes(include="number").corr()처럼 수치형만 골라 넘기는 습관이 안전합니다.

16.4 학습곡선 — 과적합을 눈으로

모델을 학습시키면(18장 Keras) epoch별 손실(loss)과 정확도가 기록됩니다. 이를 학습(train) vs 검증(validation) 두 선으로 그리면 과적합을 판단할 수 있습니다. 검증 손실이 다시 올라가기 시작하는 지점이 과적합의 신호입니다.

# history는 model.fit(...)이 반환하는 객체 (18장 참조)
epochs = range(1, 11)
train_loss = [0.9, 0.7, 0.55, 0.45, 0.38, 0.33, 0.30, 0.28, 0.27, 0.26]
val_loss   = [0.95, 0.75, 0.60, 0.52, 0.50, 0.51, 0.55, 0.60, 0.66, 0.72]

plt.figure(figsize=(6, 4))
plt.plot(epochs, train_loss, label="train")
plt.plot(epochs, val_loss, label="validation")
plt.xlabel("epoch")
plt.ylabel("loss")
plt.legend()                   # 범례 표시
plt.title("Learning Curve")
plt.show()
# 검증 손실이 5 epoch 부근부터 상승 → 과적합 시작
🎯 AICE: 딥러닝 문항에서 history.history['loss']와 history.history['val_loss']를 꺼내 두 선을 그리는 학습곡선 시각화가 마무리 단계로 나옵니다. plt.plot(..., label=...) 뒤에 plt.legend()를 빠뜨려 범례가 안 나오는 실수가 잦으니 짝으로 외우세요.

연습문제

Q1. 범주형 열 grade의 빈도를 막대그래프로 그리려 합니다. 빈칸을 채우세요.

import seaborn as sns
sns.____(data=df, x="grade")
plt.show()
정답
import seaborn as sns
sns.countplot(data=df, x="grade")
plt.show()

범주별 개수를 세는 것은 countplot입니다. 수치 분포라면 histplot을 씁니다.

Q2. 수치형 특성들의 상관계수 행렬을 heatmap으로 그리려 합니다. 빈칸을 채우세요.

corr = df.____()
sns.heatmap(corr, annot=True, cmap="coolwarm")
plt.show()
정답
corr = df.corr()
sns.heatmap(corr, annot=True, cmap="coolwarm")
plt.show()

df.corr()가 상관계수 행렬을 만들고, annot=True로 각 셀에 값을 표시합니다.

Q3. 학습곡선에서 검증 손실 선에 범례가 나오도록 하려 합니다. 빈칸을 채우세요.

plt.plot(epochs, val_loss, ____="validation")
plt.legend()
plt.show()
정답
plt.plot(epochs, val_loss, label="validation")
plt.legend()
plt.show()

plot의 label 인자로 선 이름을 지정하고, plt.legend()가 그 라벨들을 범례로 표시합니다.

이 챕터의 JS→Python 대응 한눈에 보기

JS (Chart.js 계열)Python (matplotlib/seaborn)비고
new Chart(ctx, {type:'line'})plt.plot(x, y)선 그래프
type: 'bar' + 집계sns.countplot(data, x=...)범주 빈도
히스토그램 플러그인sns.histplot(data, x=..., bins=...)수치 분포
(대응 없음)sns.boxplot(data, x=, y=)사분위·이상치
히트맵 플러그인sns.heatmap(df.corr(), annot=True)상관관계
여러 캔버스plt.subplots(행, 열)격자 배치
데이터셋 labelplt.plot(..., label=...)+plt.legend()범례

Chapter 17. Scikit-learn 모델링 프로세스

학습 목표
- train_test_split으로 데이터를 학습·평가용으로 분리할 수 있다
- 모든 모델이 공유하는 fit/predict 인터페이스를 이해하고 사용할 수 있다
- LinearRegression/LogisticRegression/DecisionTree로 회귀·분류 모델을 학습할 수 있다
- 회귀(MAE/RMSE/R²)와 분류(accuracy/f1/confusion_matrix) 평가지표를 계산할 수 있다

이제 전처리한 데이터로 모델을 만듭니다. scikit-learn의 가장 큰 미덕은 모든 모델이 똑같은 인터페이스를 쓴다는 점입니다. 회귀든 분류든 트리든, 모델 = 클래스() → 모델.fit(X, y) → 모델.predict(X)의 3단계가 동일합니다. 9장에서 배운 클래스·객체·메서드가 여기서 그대로 실전이 됩니다. 이 챕터는 AICE Associate 실기의 종착점입니다.

17.1 데이터 분리 — train_test_split

모델을 학습에 쓴 데이터로 평가하면 "외운 답"을 채점하는 셈이라 성능이 부풀려집니다. 그래서 데이터를 학습용과 평가용으로 나눕니다. train_test_split이 이를 무작위로 갈라 4개(X_train, X_test, y_train, y_test)를 한 줄로 반환합니다(7장의 다중 반환·언패킹).

import numpy as np
from sklearn.model_selection import train_test_split

X = np.arange(20).reshape(10, 2)   # 10샘플 × 2특성
y = np.arange(10)

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42
)
print(X_train.shape, X_test.shape)   # 7:3 분리
# 출력: (7, 2) (3, 2)

test_size는 평가용 비율, random_state는 재현성을 위한 난수 시드입니다. 분류에서는 stratify=y로 클래스 비율을 유지하기도 합니다.

⚠️ 함정: 반환 순서는 X_train, X_test, y_train, y_test로 고정입니다. X_train, y_train, X_test, y_test로 받으면 문법 오류 없이 실행되지만 데이터가 뒤섞여 조용히 망가집니다. 순서를 손가락에 익히세요 — 실기 최다 실수 중 하나입니다.
🎯 AICE: train_test_split(X, y, test_size=0.2, random_state=42)는 실기의 고정 관용구입니다. test_size와 random_state를 빈칸으로 두는 형태가 자주 나오며, random_state를 지정하지 않으면 채점 기준과 결과가 달라질 수 있으니 항상 명시하세요.

17.2 estimator API — fit과 predict

scikit-learn의 모든 모델은 estimator라는 공통 객체 규약을 따릅니다. 9장의 관점으로 보면, 각 모델은 클래스이고 fit·predict는 그 인스턴스의 메서드입니다. fit이 데이터에서 내부 파라미터를 학습해 객체 상태를 갱신하고, predict가 그 상태로 예측합니다.

단계코드9장 대응
생성model = LinearRegression()인스턴스 생성 __init__
학습model.fit(X, y)상태 갱신 메서드
예측model.predict(X)상태 사용 메서드
from sklearn.linear_model import LinearRegression

model = LinearRegression()          # 1. 객체 생성
model.fit(X_train, y_train)         # 2. 학습(상태 갱신)
preds = model.predict(X_test)       # 3. 예측
print(preds.shape)
# 출력: (3,)

학습으로 채워진 상태는 밑줄로 끝나는 속성(coef_, intercept_)에 저장됩니다. 밑줄 접미사는 "fit 이후에 생기는 값"이라는 scikit-learn의 관례입니다.

print(hasattr(model, "coef_"))      # fit 후 생김
# 출력: True

17.3 회귀 — LinearRegression과 평가지표

목표값이 연속 숫자(집값·점수)면 회귀입니다. 완벽히 선형인 데이터로 지표를 확인해 봅니다. y = 3x + 5 관계를 심으면 모델이 이를 되찾습니다.

X_reg = np.arange(0, 50).reshape(-1, 1).astype(float)
y_reg = (3 * X_reg.flatten() + 5)          # 정확히 선형

Xtr, Xte, ytr, yte = train_test_split(X_reg, y_reg, test_size=0.3, random_state=42)
reg = LinearRegression().fit(Xtr, ytr)
print(round(reg.coef_[0], 1), round(reg.intercept_, 1))   # 기울기, 절편 복원
# 출력: 3.0 5.0

회귀 평가지표 3종입니다. MAE(평균절대오차)는 오차의 절댓값 평균, RMSE(평균제곱근오차)는 큰 오차에 민감, R²(결정계수)는 1에 가까울수록 좋습니다.

from sklearn.metrics import mean_absolute_error, root_mean_squared_error, r2_score

pred_reg = reg.predict(Xte)
print(round(mean_absolute_error(yte, pred_reg), 2))     # 오차 거의 0
# 출력: 0.0
print(round(root_mean_squared_error(yte, pred_reg), 2))
# 출력: 0.0
print(round(r2_score(yte, pred_reg), 3))                # 완벽 예측=1.0
# 출력: 1.0
⚠️ 함정: 예전 자료의 mean_squared_error(y, pred, squared=False)로 RMSE를 구하는 방식은 최신 scikit-learn에서 제거되었습니다. RMSE는 전용 함수 root_mean_squared_error를 쓰거나 np.sqrt(mean_squared_error(...))로 계산하세요.

17.4 분류 — LogisticRegression과 평가지표

목표값이 범주(합격/불합격, 개/고양이)면 분류입니다. 이름과 달리 LogisticRegression은 분류 모델입니다. 명확히 나뉘는 데이터로 지표를 확인합니다.

from sklearn.linear_model import LogisticRegression

# 두 그룹이 뚜렷이 분리된 데이터
X_clf = np.array([[i] for i in [1, 2, 3, 4, 20, 21, 22, 23]], dtype=float)
y_clf = np.array([0, 0, 0, 0, 1, 1, 1, 1])

Xtr, Xte, ytr, yte = train_test_split(X_clf, y_clf, test_size=0.5, random_state=42)
clf = LogisticRegression().fit(Xtr, ytr)
pred_clf = clf.predict(Xte)
print(pred_clf.tolist())
# 출력: [0, 1, 0, 1]

분류 평가지표입니다. accuracy(정확도)는 맞힌 비율, f1은 정밀도·재현율의 조화평균(불균형 데이터에서 중요), confusion_matrix는 실제 vs 예측 교차표입니다.

from sklearn.metrics import accuracy_score, f1_score, confusion_matrix

print(accuracy_score(yte, pred_clf))     # 완벽 분리=1.0
# 출력: 1.0
print(f1_score(yte, pred_clf))
# 출력: 1.0
print(confusion_matrix(yte, pred_clf).shape)   # 2×2 (클래스 2개)
# 출력: (2, 2)
🎯 AICE: 회귀 문항은 r2_score·RMSE를, 분류 문항은 accuracy_score·f1_score·confusion_matrix를 계산하라고 요구합니다. 모든 지표 함수의 인자 순서가 (정답 y_test, 예측 pred)임을 기억하세요 — 순서를 바꿔도 값이 나오는 지표가 있어 조용히 틀리기 쉽습니다.

17.5 DecisionTree — 같은 인터페이스, 다른 모델

의사결정나무는 회귀·분류 모두 가능하며, 인터페이스는 앞과 완전히 동일합니다. 모델 클래스만 바꾸면 됩니다 — 이것이 estimator API의 힘입니다.

from sklearn.tree import DecisionTreeClassifier

tree = DecisionTreeClassifier(max_depth=3, random_state=42)
tree.fit(Xtr, ytr)
print(accuracy_score(yte, tree.predict(Xte)))
# 출력: 1.0

max_depth(트리 깊이) 같은 값은 학습으로 정해지지 않고 사람이 지정하는 하이퍼파라미터입니다(21장에서 자동 탐색). 트리 계열은 어떤 특성이 중요했는지도 알려 줍니다.

print(tree.feature_importances_.shape)   # 특성 수만큼
# 출력: (1,)
⚠️ 함정: DecisionTreeClassifier(분류)와 DecisionTreeRegressor(회귀)는 다른 클래스입니다. 목표값이 범주면 Classifier, 연속값이면 Regressor입니다. 회귀 문제에 Classifier를 쓰면 연속값을 범주로 오인해 엉뚱한 결과가 나오니 목표변수 종류를 먼저 확인하세요.

연습문제

Q1. 데이터를 8:2로 분리하되 재현 가능하도록 시드를 42로 고정하려 합니다. 빈칸을 채우세요.

from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=____, random_state=____
)
정답
import numpy as np
from sklearn.model_selection import train_test_split
X = np.arange(20).reshape(10, 2); y = np.arange(10)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)
print(X_test.shape[0])
# 출력: 2

test_size=0.2가 20%를 평가용으로, random_state=42가 분리를 고정합니다.

Q2. 선형회귀 모델을 학습시키고 예측하려 합니다. 빈칸을 채우세요.

from sklearn.linear_model import LinearRegression
model = LinearRegression()
model.____(X_train, y_train)     # 학습
preds = model.____(X_test)       # 예측
정답
import numpy as np
from sklearn.linear_model import LinearRegression
X_train = np.arange(10).reshape(-1, 1).astype(float)
y_train = 2 * X_train.flatten()
model = LinearRegression()
model.fit(X_train, y_train)
preds = model.predict(X_train)
print(round(model.coef_[0], 1))
# 출력: 2.0

fit으로 학습하고 predict로 예측합니다. 모든 estimator가 공유하는 인터페이스입니다.

Q3. 분류 결과의 정확도를 계산하려 합니다. 인자 순서에 주의해 빈칸을 채우세요.

from sklearn.metrics import accuracy_score
acc = accuracy_score(____, ____)   # (정답, 예측)
정답
from sklearn.metrics import accuracy_score
y_test = [0, 1, 1, 0]
pred = [0, 1, 1, 0]
acc = accuracy_score(y_test, pred)
print(acc)
# 출력: 1.0

평가지표 함수는 (정답 y_test, 예측 pred) 순서로 넘깁니다.

이 챕터의 JS→Python 대응 한눈에 보기

개념Python (scikit-learn)비고
데이터 분리train_test_split(X, y, test_size=, random_state=)반환 순서 고정
객체 생성 (9장 __init__)model = LinearRegression()모델 = 클래스 인스턴스
학습 메서드model.fit(X_train, y_train)상태(coef_) 갱신
예측 메서드model.predict(X_test)학습된 상태 사용
회귀 지표mean_absolute_error / root_mean_squared_error / r2_score(정답, 예측) 순
분류 지표accuracy_score / f1_score / confusion_matrix(정답, 예측) 순
하이퍼파라미터DecisionTreeClassifier(max_depth=3)사람이 지정

Chapter 18. Keras 딥러닝 기초

학습 목표
- Sequential과 Dense로 신경망 구조를 쌓을 수 있다
- compile로 optimizer/loss/metrics를 설정할 수 있다
- fit의 epochs/batch_size/validation로 학습을 제어할 수 있다
- EarlyStopping/ModelCheckpoint 콜백과 evaluate로 학습을 관리·평가할 수 있다

딥러닝은 AICE Associate 실기의 마지막 관문입니다. 다행히 Keras는 17장의 scikit-learn과 정신 모델이 같습니다: 모델을 만들고(구성) → 설정하고(compile) → 학습하고(fit) → 평가한다(evaluate). 층(layer)을 순서대로 쌓는 것은 함수를 합성하거나 미들웨어를 체이닝하던 감각과 비슷합니다. 새로 외울 것은 "층의 종류와 compile 인자"뿐입니다.

📦 설치 안내: 이 챕터를 로컬에서 실습하려면 TensorFlow가 필요합니다(기본 파이썬 환경에는 없을 수 있습니다).
```bash
pip install tensorflow
```
from tensorflow import keras로 불러오며, 최신 Keras 3 기준으로 설명합니다. 예제는 재현성을 위해 시드를 고정하고, 책에서는 학습 시간을 줄이려 epochs를 작게(1~3) 두었습니다.

18.1 Sequential과 Dense — 층을 쌓기

가장 기본 구조는 층을 일렬로 쌓는 Sequential입니다. 각 Dense(완전연결층)는 뉴런 수와 활성화 함수를 받습니다. JS로 비유하면 입력을 순서대로 통과시키는 변환 파이프라인을 선언하는 것과 같습니다.

import os
os.environ.setdefault("TF_CPP_MIN_LOG_LEVEL", "3")   # TensorFlow 로그 최소화
import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers

np.random.seed(42)
tf.random.set_seed(42)                        # 재현성 고정

model = keras.Sequential([
    keras.Input(shape=(10,)),                 # 입력 특성 10개
    layers.Dense(64, activation="relu"),      # 은닉층 64뉴런
    layers.Dense(32, activation="relu"),      # 은닉층 32뉴런
    layers.Dense(1, activation="sigmoid"),    # 출력층: 이진분류
])
print(model.count_params())   # 전체 학습 파라미터 수
# 출력: 2817
model.summary()               # 층 구조와 파라미터 요약 출력

활성화 함수는 층에 비선형성을 부여합니다. 은닉층에는 보통 relu, 출력층은 문제에 따라 다릅니다: 이진분류는 sigmoid(0~1 확률), 다중분류는 softmax(합이 1인 분포), 회귀는 활성화 없음(선형).

문제 유형출력층 뉴런 수출력층 활성화
회귀1없음(linear)
이진분류1sigmoid
다중분류(N클래스)Nsoftmax
⚠️ 함정: 첫 층에서 keras.Input(shape=(특성수,))로 입력 형태를 알려 줘야 합니다. shape는 샘플 하나의 형태라 배치 크기는 넣지 않습니다. 특성이 10개면 (10,)이지 (샘플수, 10)이 아닙니다. 튜플 안 쉼표((10,))를 빠뜨리면 정수로 해석돼 에러가 납니다(4장 튜플).

18.2 compile — 학습 방법 설정

모델의 뼈대를 만들었으면 어떻게 학습할지를 정합니다. 세 가지를 지정합니다: optimizer(가중치 갱신 방법, 보통 "adam"), loss(오차 정의), metrics(모니터링할 지표).

model.compile(
    optimizer="adam",
    loss="binary_crossentropy",     # 이진분류용 손실
    metrics=["accuracy"],
)

loss는 문제 유형과 반드시 짝이 맞아야 합니다. 이것이 딥러닝 실기에서 가장 자주 틀리는 지점입니다.

문제 유형loss
회귀"mse" (또는 "mae")
이진분류"binary_crossentropy"
다중분류(정수 라벨)"sparse_categorical_crossentropy"
다중분류(원-핫 라벨)"categorical_crossentropy"
🎯 AICE: model.compile(optimizer='adam', loss='...', metrics=['accuracy'])는 실기 고정 패턴이며 loss 자리가 빈칸으로 자주 나옵니다. 출력층 활성화와 loss의 짝(sigmoid↔binary_crossentropy, softmax↔categorical_crossentropy)을 세트로 외우세요. 목표변수가 정수 라벨이면 sparse_를 붙입니다.

18.3 fit — 학습 실행

fit으로 학습합니다. 17장 scikit-learn의 fit과 이름이 같지만, 딥러닝은 데이터를 여러 번 반복 학습하므로 인자가 더 많습니다.

# 실습용 소형 데이터 (200샘플 × 10특성, 0/1 이진 라벨)
X_train = np.random.rand(200, 10).astype("float32")
y_train = (X_train[:, 0] > 0.5).astype("int32")

history = model.fit(
    X_train, y_train,
    epochs=3,                 # 책 실습용 축소 (실제로는 수십~수백)
    batch_size=32,
    validation_split=0.2,     # 학습 데이터의 20%를 검증에 사용
    verbose=0,                # 진행 로그 생략
)
print(len(history.history["loss"]))   # 기록된 epoch 수
# 출력: 3

fit은 history 객체를 반환하며, history.history에 epoch별 손실·지표가 딕셔너리로 담깁니다. 16장의 학습곡선이 이 값을 그리는 것입니다.

print("val_loss" in history.history)   # 검증 손실이 기록되었는가
# 출력: True
print(sorted(history.history.keys()))  # 기록된 지표 이름들
# 출력: ['accuracy', 'loss', 'val_accuracy', 'val_loss']
⚠️ 함정: validation_split은 학습 데이터의 뒷부분을 순서대로 떼어 검증에 씁니다. 데이터가 클래스별로 정렬돼 있으면 검증셋이 한 클래스로 쏠려 성능이 왜곡됩니다. 미리 섞여 있지 않다면 17장의 train_test_split으로 별도 검증셋을 만들어 validation_data=(X_val, y_val)로 넘기는 편이 안전합니다.

18.4 콜백 — EarlyStopping과 ModelCheckpoint

콜백은 학습 도중 자동으로 개입하는 훅입니다(JS의 이벤트 콜백과 같은 발상). 두 가지가 필수입니다. EarlyStopping은 검증 성능이 나아지지 않으면 학습을 조기 종료하고, ModelCheckpoint는 가장 좋았던 시점의 모델을 파일로 저장합니다.

import tempfile, shutil
ckpt_path = os.path.join(tempfile.mkdtemp(), "best_model.keras")   # 실기에선 "best_model.keras" 같은 파일명

early = keras.callbacks.EarlyStopping(
    monitor="val_loss",         # 검증 손실을 감시
    patience=5,                 # 5 epoch 개선 없으면 중단
    restore_best_weights=True,  # 최적 가중치로 복원
)
checkpoint = keras.callbacks.ModelCheckpoint(
    ckpt_path,                  # Keras 3: 확장자 .keras 필수
    monitor="val_loss",
    save_best_only=True,        # 가장 좋은 시점만 저장
)

history = model.fit(
    X_train, y_train,
    epochs=3,                        # 책 실습용 축소
    batch_size=32,
    validation_split=0.2,
    callbacks=[early, checkpoint],   # 리스트로 전달
    verbose=0,
)
print(os.path.exists(ckpt_path))     # 최적 모델이 파일로 저장됨
# 출력: True
shutil.rmtree(os.path.dirname(ckpt_path))   # 실습 정리 (실기에서는 불필요)
⚠️ 함정: patience는 "몇 epoch까지 참을지"입니다. 너무 작으면 잠깐의 정체에도 조기 종료되고, 너무 크면 과적합이 진행됩니다. 또 restore_best_weights=True를 빠뜨리면 조기 종료 시점의(최적이 아닌) 가중치가 남으니 함께 지정하세요. Keras 3에서 ModelCheckpoint 경로는 .keras 확장자로 끝나야 합니다.

18.5 evaluate와 predict — 평가와 예측

학습이 끝나면 평가용 데이터로 최종 성능을 확인합니다. evaluate는 compile에서 지정한 loss와 metrics를 반환하고, predict는 예측값을 냅니다 — 여기서도 scikit-learn과 인터페이스가 같습니다.

# 평가용 소형 데이터
X_test = np.random.rand(40, 10).astype("float32")
y_test = (X_test[:, 0] > 0.5).astype("int32")

loss, acc = model.evaluate(X_test, y_test, verbose=0)   # [손실, 정확도]
print(0.0 <= acc <= 1.0)      # 정확도는 항상 0~1 범위 (값은 환경별 편차)
# 출력: True

probs = model.predict(X_test, verbose=0)     # 확률(sigmoid 출력)
preds = (probs > 0.5).astype("int32")         # 0.5 기준 이진 변환
print(probs.shape)                            # (샘플 수, 1)
# 출력: (40, 1)
🎯 AICE: 딥러닝 문항의 마무리는 model.evaluate(X_test, y_test)로 성능을 출력하고, history로 학습곡선을 그리는 흐름입니다. evaluate는 compile의 metrics에 따라 반환값 개수가 달라지므로(손실 + 각 지표), 정확도까지 원하면 compile에 metrics=['accuracy']를 반드시 넣어야 합니다.

연습문제

Q1. 특성 8개를 입력받아 이진분류하는 3층 신경망을 만들려 합니다. 빈칸을 채우세요.

from tensorflow import keras
from tensorflow.keras import layers
model = keras.Sequential([
    keras.Input(shape=(____,)),
    layers.Dense(16, activation="relu"),
    layers.Dense(1, activation="____"),   # 이진분류 출력
])
정답
model = keras.Sequential([
    keras.Input(shape=(8,)),
    layers.Dense(16, activation="relu"),
    layers.Dense(1, activation="sigmoid"),
])

입력 특성이 8개이므로 (8,), 이진분류 출력층은 sigmoid입니다.

Q2. 이진분류 모델을 컴파일하려 합니다. 빈칸을 채우세요.

model.compile(
    optimizer="adam",
    loss="____",              # 이진분류 손실
    metrics=["accuracy"],
)
정답
model.compile(
    optimizer="adam",
    loss="binary_crossentropy",
    metrics=["accuracy"],
)

이진분류의 손실은 binary_crossentropy이고, 출력층 sigmoid와 짝을 이룹니다.

Q3. 검증 손실이 5 epoch 동안 개선되지 않으면 학습을 멈추는 콜백을 만들려 합니다. 빈칸을 채우세요.

early = keras.callbacks.EarlyStopping(
    monitor="____",
    patience=____,
    restore_best_weights=True,
)
정답
early = keras.callbacks.EarlyStopping(
    monitor="val_loss",
    patience=5,
    restore_best_weights=True,
)

검증 손실 val_loss를 감시하고, patience=5만큼 참은 뒤 최적 가중치로 복원합니다.

이 챕터의 JS→Python 대응 한눈에 보기

개념Python (Keras)비고
층 쌓기keras.Sequential([...])순차 파이프라인
완전연결층layers.Dense(뉴런, activation=)은닉층 relu
입력 형태keras.Input(shape=(특성수,))배치 크기 제외
학습 설정model.compile(optimizer, loss, metrics)loss↔출력활성화 짝
학습 (17장 fit 확장)model.fit(X, y, epochs=, batch_size=)history 반환
이벤트 콜백EarlyStopping / ModelCheckpointcallbacks=[...]
평가 (17장과 동일)model.evaluate(X_test, y_test)[loss, metrics]

Chapter 19. 고급 전처리와 특성 공학

학습 목표
- 기존 컬럼에서 파생·상호작용 특성을 만들어 모델에 신호를 더할 수 있다.
- 범주형 변수를 상황에 맞게(원-핫·빈도·순서) 인코딩할 수 있다.
- 스케일링과 결측·이상치 처리를 데이터 누수 없이 train에만 fit하여 적용할 수 있다.
- SelectKBest·모델 중요도로 특성을 선택하고, 불균형 데이터의 개념을 설명할 수 있다.

Part 2에서 fillna·get_dummies·StandardScaler로 "돌아가는" 전처리를 배웠습니다. Professional 트랙은 여기서 한 걸음 더 나아가 모델 성능을 끌어올리는 전처리를 요구합니다. 이 장의 코드는 numpy·pandas·scikit-learn만 사용하므로 전부 실행 가능합니다.

먼저 이 장 전체에서 쓸 합성 데이터를 만듭니다.

import numpy as np
import pandas as pd

np.random.seed(42)
n = 200
df = pd.DataFrame({
    "age": np.random.randint(20, 60, n),
    "income": np.random.randint(2000, 9000, n),
    "city": np.random.choice(["seoul", "busan", "daegu"], n),
    "signup_days": np.random.randint(1, 400, n),
})
print(df.shape)
# 출력: (200, 4)

19.1 특성 생성 — 파생과 상호작용

특성 공학의 출발점은 "도메인 지식으로 컬럼을 조합"하는 일입니다. JS에서 배열을 .map()으로 가공하듯, pandas에서는 열 단위 벡터 연산으로 새 컬럼을 만듭니다. 반복문은 필요 없습니다.

JavaScriptPython
``const r = rows.map(x => ({...x, ratio: x.income / x.age}));````df["ratio"] = df["income"] / df["age"]``

income / age처럼 두 특성을 곱하거나 나눈 값을 상호작용 특성(interaction feature)이라 합니다. 선형 모델은 특성 간 곱셈을 스스로 학습하지 못하므로, 사람이 미리 만들어주면 표현력이 올라갑니다.

df["income_per_age"] = df["income"] / df["age"]      # 파생 비율
df["age_x_days"] = df["age"] * df["signup_days"]      # 상호작용
df["is_new"] = (df["signup_days"] < 30).astype(int)   # 불리언 → 0/1 플래그
print(df.shape[1])
# 출력: 7
⚠️ JS 함정: (df["signup_days"] < 30)은 원소별 불리언 Series입니다. JS의 if (arr < 30)처럼 배열 전체를 하나의 진릿값으로 착각하지 마세요. 여기에 .astype(int)를 붙여야 True/False가 1/0 정수 특성이 됩니다.

19.2 범주형 고급 인코딩

Part 2에서는 get_dummies(원-핫) 하나만 썼습니다. 실무·시험에서는 카디널리티(고윳값 개수)에 따라 인코딩을 골라야 합니다.

from sklearn.preprocessing import OneHotEncoder

ohe = OneHotEncoder(sparse_output=False, handle_unknown="ignore")
city_ohe = ohe.fit_transform(df[["city"]])   # 2차원 입력 df[["city"]]에 주의
print(city_ohe.shape)
# 출력: (200, 3)

빈도 인코딩은 pandas value_counts + map으로 한 줄입니다.

freq = df["city"].value_counts(normalize=True)   # 각 city의 등장 비율
df["city_freq"] = df["city"].map(freq)
print(df["city_freq"].nunique())
# 출력: 3
⚠️ 함정: OneHotEncoder는 입력으로 2차원을 요구합니다. df["city"](1차원 Series)를 넣으면 에러가 납니다. 대괄호를 두 번 감싼 df[["city"]](DataFrame)을 넘기세요 — JS의 [x] 배열 감싸기와 발상은 같지만 축(axis) 개념이 다릅니다.

19.3 스케일링 심화 — 누수 없이 fit하기

스케일러 종류는 데이터 분포로 고릅니다. StandardScaler(평균 0·표준편차 1)는 정규분포에 가까울 때, RobustScaler(중앙값·IQR 기준)는 이상치가 많을 때 유리합니다. 핵심은 종류가 아니라 fit 시점입니다.

from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split

num_cols = ["age", "income", "signup_days"]
X_train, X_test = train_test_split(df[num_cols], test_size=0.3, random_state=42)

scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train)   # 통계는 train에서만 학습
X_test_s = scaler.transform(X_test)         # test는 transform만 (재학습 금지)
print(abs(X_train_s.mean()) < 1e-9)
# 출력: True
⚠️ 함정: fit_transform을 전체 데이터에 한 번 돌린 뒤 나누면, test의 평균·분산 정보가 train 전처리에 새어 들어갑니다. 이것이 데이터 누수(data leakage)입니다. 반드시 train에 fit_transform, test에 transform만 하세요. 21장의 Pipeline이 이 순서를 강제해 줍니다.
🎯 AICE: Professional 실기는 "train에 fit_transform, test에 transform" 빈칸을 자주 냅니다. 두 메서드를 손으로 구분해 쓸 수 있어야 합니다.

19.4 결측·이상치 전략

Part 2의 fillna는 값을 직접 지정했습니다. SimpleImputer는 통계값(평균·중앙값·최빈값)을 학습해 채우므로, 스케일러처럼 누수 없이 train/test에 적용됩니다.

from sklearn.impute import SimpleImputer

df2 = df.copy()
df2.loc[:9, "income"] = np.nan          # 앞 10행을 결측으로 만들기
imp = SimpleImputer(strategy="median")
df2["income"] = imp.fit_transform(df2[["income"]])
print(df2["income"].isnull().sum())
# 출력: 0

이상치는 IQR 규칙(Q1−1.5·IQR ~ Q3+1.5·IQR 밖)으로 탐지합니다. 박스플롯 수염과 동일한 기준입니다.

q1, q3 = df["income"].quantile([0.25, 0.75])
iqr = q3 - q1
lower, upper = q1 - 1.5 * iqr, q3 + 1.5 * iqr
mask = df["income"].between(lower, upper)   # 정상 범위 = True
print("outliers:", int((~mask).sum()))
# 출력: outliers:

19.5 특성 선택

특성이 많다고 좋은 게 아닙니다. 노이즈 특성은 과적합을 부릅니다. SelectKBest는 타깃과의 통계적 관련성(예: f_classif) 상위 k개를 고릅니다.

from sklearn.feature_selection import SelectKBest, f_classif

y = (df["income"] > df["income"].median()).astype(int)   # 이진 타깃
num_feats = df[["age", "income", "signup_days", "income_per_age"]]
selector = SelectKBest(score_func=f_classif, k=2)
selector.fit(num_feats, y)
chosen = num_feats.columns[selector.get_support()].tolist()
print(len(chosen))
# 출력: 2

모델 기반 선택도 흔합니다. 트리 모델의 feature_importances_(20장에서 상술)로 중요도를 매겨 하위 특성을 버립니다.

from sklearn.ensemble import RandomForestClassifier

rf = RandomForestClassifier(n_estimators=30, random_state=42)
rf.fit(num_feats, y)
importances = pd.Series(rf.feature_importances_, index=num_feats.columns)
print(round(importances.sum(), 4))   # 중요도 합은 항상 1
# 출력: 1.0
🎯 AICE: feature_importances_는 Professional에서 "어떤 특성이 예측에 기여했는가"를 묻는 문항으로 등장합니다. pd.Series(model.feature_importances_, index=X.columns).sort_values() 관용구를 익혀두세요.

19.6 불균형 데이터 개요

분류에서 한 클래스가 90%를 차지하면(예: 정상 180 : 이상 20), 모델이 전부 "정상"이라 찍어도 정확도 90%가 나옵니다. 이것이 불균형(imbalanced) 데이터 문제입니다.

y_imb = np.array([0] * 180 + [1] * 20)
print(round((y_imb == 1).mean(), 2))   # 소수 클래스 비율
# 출력: 0.1

대응은 크게 세 갈래입니다: (1) 소수 클래스 복제·합성(오버샘플링, SMOTE 등), (2) 다수 클래스 축소(언더샘플링), (3) 손실 함수에서 소수 클래스에 가중치를 주는 class_weight="balanced". sklearn 내장 방식인 (3)이 가장 간단합니다.

from sklearn.linear_model import LogisticRegression

clf = LogisticRegression(class_weight="balanced", max_iter=1000)
print(clf.get_params()["class_weight"])
# 출력: balanced

불균형 데이터에서는 정확도(accuracy) 대신 f1·recall로 평가해야 합니다(22장 참조).

🎯 AICE (출제 이력 불확실): 불균형 데이터 처리(SMOTE·class_weight)는 AICE Professional 공개 범위에 명시되어 있지 않아 회차별 출제 편차가 있을 수 있습니다. imblearn의 SMOTE는 별도 설치가 필요하므로, 시험 환경에서 우선순위는 sklearn 내장 class_weight와 평가지표(f1) 쪽에 두는 편이 안전합니다.

연습문제

Q1. train에만 통계를 학습시키고 test에는 적용만 하도록 빈칸을 채우세요.

scaler = StandardScaler()
X_train_s = scaler.____(X_train)   # 학습 + 변환
X_test_s = scaler.____(X_test)     # 변환만
정답
scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train)
X_test_s = scaler.transform(X_test)

fit_transform은 train에, transform은 test에. 반대로 하거나 test에 fit하면 데이터 누수가 발생합니다.

Q2. 타깃과의 관련성 상위 3개 특성을 선택하도록 빈칸을 채우세요.

from sklearn.feature_selection import SelectKBest, f_classif
selector = SelectKBest(score_func=f_classif, k=____)
selector.fit(X, y)
cols = X.columns[selector.____()]
정답
selector = SelectKBest(score_func=f_classif, k=3)
selector.fit(X, y)
cols = X.columns[selector.get_support()]

k는 선택할 특성 수, get_support()는 선택 여부 불리언 마스크를 반환합니다.

이 챕터의 JS→Python 대응 한눈에 보기

JSPython비고
rows.map(x => x.a / x.b)df["a"] / df["b"]파생 특성은 열 벡터 연산
arr < 30 (전체 비교 불가)(df["c"] < 30).astype(int)원소별 불리언 → 0/1 플래그
[x]로 배열 감싸기df[["col"]]인코더는 2차원 입력 요구
수동 정규화 함수StandardScaler().fit_transformtrain fit / test transform 분리
—SimpleImputer, SelectKBestJS 대응 없음, sklearn 고유

Chapter 20. 앙상블 모델

학습 목표
- 배깅(bagging)과 부스팅(boosting)의 원리와 차이를 설명할 수 있다.
- RandomForestClassifier·GradientBoostingClassifier를 학습·평가할 수 있다.
- feature_importances_로 특성 중요도를 추출·해석할 수 있다.
- XGBoost·LightGBM의 API 형태를 읽고 sklearn 계열과의 관계를 안다.

한 개의 결정 트리(17장)는 데이터에 과하게 맞아 흔들립니다. 앙상블(ensemble)은 약한 예측기 여러 개를 결합해 안정적이고 강한 예측기를 만드는 기법입니다. 개념적으로는 "여러 개의 서로 다른 휴리스틱을 투표·가중합으로 합친다"는 발상으로, JS에서 여러 판정 함수의 결과를 종합하던 것과 닮았습니다. sklearn 계열(RandomForest·GradientBoosting)은 실행 가능하고, XGBoost·LightGBM은 이 환경에 미설치라 해당 코드만 no-run으로 표기합니다.

import numpy as np
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split

X, y = make_classification(n_samples=300, n_features=8, n_informative=5,
                           random_state=42)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42)
print(X_train.shape)
# 출력: (210, 8)

20.1 배깅 vs 부스팅 — 두 결합 전략

전략학습 방식목표대표 모델
배깅(Bagging)데이터를 부트스트랩 샘플링해 여러 트리를 병렬·독립 학습 후 투표분산↓ (과적합 완화)RandomForest
부스팅(Boosting)앞 모델의 오차를 다음 모델이 순차적으로 보정편향↓ (정확도↑)GradientBoosting, XGBoost, LightGBM

배깅은 독립된 예측기들의 다수결입니다. 부스팅은 "직전 모델이 틀린 샘플에 집중"하는 반복 개선으로, JS에서 오차를 조금씩 줄여가며 파라미터를 갱신하던 반복 루프와 사고방식이 같습니다. 병렬이 아니라 순차라서 느리지만 대체로 더 정확합니다.

20.2 RandomForest — 대표 배깅 모델

RandomForestClassifier는 17장의 결정 트리와 완전히 같은 fit/predict 인터페이스를 씁니다. 트리를 숲으로 바꾸기만 하면 됩니다.

from sklearn.ensemble import RandomForestClassifier

rf = RandomForestClassifier(n_estimators=50, random_state=42)
rf.fit(X_train, y_train)
acc = rf.score(X_test, y_test)   # score = 분류 정확도
print(round(acc, 2) > 0.5)
# 출력: True

단일 트리보다 숲이 왜 나은지 비교해 봅니다. 같은 데이터에서 결정 트리 한 그루는 test 성능이 출렁이지만, 숲은 이를 평균 내어 안정됩니다.

from sklearn.tree import DecisionTreeClassifier

tree = DecisionTreeClassifier(random_state=42).fit(X_train, y_train)
print(tree.score(X_test, y_test) <= 1.0)
# 출력: True
⚠️ 함정: n_estimators(트리 개수)는 크게 잡을수록 안정적이지만 그만큼 느려집니다. JS에서 루프 반복 횟수를 무작정 늘리지 않듯, 100~300 선에서 타협합니다. random_state를 고정하지 않으면 부트스트랩 샘플링이 매번 달라져 결과가 재현되지 않습니다.

20.3 GradientBoosting — 대표 부스팅 모델

GradientBoostingClassifier도 인터페이스는 동일합니다. 다른 점은 트리를 순차적으로 쌓아 오차를 줄인다는 것입니다.

from sklearn.ensemble import GradientBoostingClassifier

gb = GradientBoostingClassifier(n_estimators=50, learning_rate=0.1,
                                random_state=42)
gb.fit(X_train, y_train)
print(gb.n_estimators)
# 출력: 50

learning_rate(학습률)는 각 트리가 오차를 얼마나 반영할지 정하는 부스팅 고유 파라미터입니다. 작을수록 신중하게(과적합↓) 학습하지만 더 많은 트리가 필요합니다.

🎯 AICE: Professional 실기에서 RandomForestClassifier(n_estimators=...)와 GradientBoostingClassifier(learning_rate=..., n_estimators=...)의 생성자 인자를 빈칸으로 냅니다. 분류는 ...Classifier, 회귀는 ...Regressor로 이름만 바뀐다는 점을 함께 외우세요.

20.4 특성 중요도

앙상블 모델의 큰 장점은 어떤 특성이 예측에 기여했는지 알려준다는 점입니다. feature_importances_는 합이 1인 배열로, pd.Series에 담아 정렬하면 읽기 좋습니다.

import pandas as pd

importances = pd.Series(rf.feature_importances_).sort_values(ascending=False)
print(round(importances.sum(), 2))   # 중요도 총합은 1
# 출력: 1.0
top_feature = importances.index[0]   # 가장 중요한 특성의 인덱스
print(top_feature >= 0)
# 출력: True
⚠️ JS 함정: feature_importances_는 끝에 언더스코어가 붙습니다. sklearn 관례상 학습 후 생성된 속성에는 _가 붙습니다(coef_, classes_도 동일). fit 전에 접근하면 에러가 나므로, JS에서 초기화 전 프로퍼티를 읽는 실수와 같은 함정입니다.

20.5 XGBoost·LightGBM

XGBoost와 LightGBM은 부스팅을 고도로 최적화한 별도 라이브러리로, 캐글·실무에서 표준처럼 쓰입니다. 설치는 pip install xgboost lightgbm입니다.

⚙️ 설치 팁: 두 라이브러리는 OpenMP 런타임에 의존합니다. macOS에서 Library not loaded: libomp 오류가 나면 brew install libomp로 해결하세요(Linux·Windows 휠에는 대개 포함).

API는 sklearn과 호환되게 설계되어 fit/predict/feature_importances_를 그대로 씁니다. 즉 17장의 estimator 인터페이스만 알면 라이브러리를 갈아끼우기만 하면 됩니다.

from xgboost import XGBClassifier

xgb = XGBClassifier(n_estimators=30, learning_rate=0.1,
                    max_depth=3, random_state=42)
xgb.fit(X_train, y_train)
pred = xgb.predict(X_test)
print(xgb.feature_importances_.shape)   # 특성 수만큼, sklearn과 동일한 인터페이스
# 출력: (8,)
from lightgbm import LGBMClassifier

lgbm = LGBMClassifier(n_estimators=30, learning_rate=0.1,
                      random_state=42, verbose=-1)   # verbose=-1: 학습 로그 억제
lgbm.fit(X_train, y_train)
pred = lgbm.predict(X_test)
print(pred.shape)
# 출력: (90,)
🎯 AICE (출제 이력 불확실): XGBoost·LightGBM의 AICE 실기 등장 여부는 회차별 편차가 있어 공식 범위상 확실하지 않습니다. 등장하더라도 위처럼 sklearn과 같은 fit/predict 형태이므로, sklearn 앙상블(RandomForest·GradientBoosting)을 확실히 익히면 전이가 쉽습니다. 시험 대비 우선순위는 sklearn 계열에 두세요.

연습문제

Q1. 트리 100개짜리 RandomForest 분류기를 학습하도록 빈칸을 채우세요.

from sklearn.ensemble import ____
model = RandomForestClassifier(____=100, random_state=42)
model.____(X_train, y_train)
정답
from sklearn.ensemble import RandomForestClassifier
model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train, y_train)

n_estimators가 트리 개수, fit으로 학습합니다.

Q2. 학습된 모델의 특성 중요도를 내림차순 Series로 만드세요.

import pandas as pd
imp = pd.Series(model.____, index=X.columns)
imp = imp.sort_values(ascending=____)
정답
imp = pd.Series(model.feature_importances_, index=X.columns)
imp = imp.sort_values(ascending=False)

feature_importances_(언더스코어 필수), ascending=False로 큰 값이 위로 옵니다.

이 챕터의 JS→Python 대응 한눈에 보기

JSPython비고
여러 판정 함수 다수결배깅 / RandomForestClassifier병렬·독립 트리의 투표
오차 줄이는 반복 개선 루프부스팅 / GradientBoostingClassifier순차적 오차 보정
초기화 전 프로퍼티 접근 오류fit 전 feature_importances_ 접근 오류_ 접미사 = 학습 후 속성
라이브러리 교체XGBoost/LightGBM (동일 fit/predict)estimator 인터페이스 공통

Chapter 21. 하이퍼파라미터 튜닝과 Pipeline

학습 목표
- 하이퍼파라미터와 학습된 파라미터의 차이를 구분할 수 있다.
- GridSearchCV·RandomizedSearchCV로 최적 조합을 탐색할 수 있다.
- Pipeline으로 전처리와 모델을 하나의 객체로 묶을 수 있다.
- ColumnTransformer로 열 종류별 전처리를 구성하고 데이터 누수를 원천 차단할 수 있다.

모델에는 두 종류의 값이 있습니다. fit이 데이터로 학습하는 파라미터(트리 분기 기준, 회귀 계수)와, 사람이 학습 전에 정해주는 하이퍼파라미터(n_estimators, max_depth, learning_rate)입니다. 이 장은 후자를 자동으로 탐색하고, 전처리와 모델을 누수 없이 묶는 법을 다룹니다. 모두 sklearn만 쓰므로 실행 가능합니다.

import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split

np.random.seed(42)
n = 200
df = pd.DataFrame({
    "age": np.random.randint(20, 60, n),
    "income": np.random.randint(2000, 9000, n),
    "city": np.random.choice(["seoul", "busan", "daegu"], n),
})
y = (df["income"] + df["age"] * 50 > 5000).astype(int)
X_train, X_test, y_train, y_test = train_test_split(
    df, y, test_size=0.3, random_state=42)
print(X_train.shape)
# 출력: (140, 3)

21.1 GridSearchCV — 격자 전수 탐색

하이퍼파라미터 후보들을 딕셔너리로 주면, GridSearchCV가 모든 조합 × 교차검증을 돌려 최고 성능 조합을 찾습니다. JS로 치면 중첩 for 루프로 파라미터를 스윕하되, 각 조합을 교차검증으로 공정하게 채점하는 것입니다.

from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import GridSearchCV

param_grid = {"n_estimators": [10, 30], "max_depth": [2, 4]}   # 2×2 = 4조합
grid = GridSearchCV(RandomForestClassifier(random_state=42),
                    param_grid, cv=3)
grid.fit(X_train[["age", "income"]], y_train)
print(sorted(grid.best_params_.keys()))
# 출력: ['max_depth', 'n_estimators']

탐색이 끝나면 best_params_(최적 조합)·best_score_(교차검증 평균 점수)·best_estimator_(그 조합으로 재학습된 모델)를 꺼낼 수 있습니다.

print(0.0 <= grid.best_score_ <= 1.0)
# 출력: True
⚠️ 함정: 격자 크기는 곱셈으로 폭발합니다. 파라미터 3개에 각 5후보면 5³=125조합, cv=5면 총 625회 학습입니다. JS에서 중첩 루프 복잡도를 계산하듯, 후보를 넣기 전에 조합 수를 곱해보세요.

21.2 RandomizedSearchCV — 무작위 표본 탐색

격자가 너무 크면 전수 탐색 대신 RandomizedSearchCV로 n_iter개만 무작위 추출해 시험합니다. 넓은 공간을 적은 비용으로 훑을 때 효율적입니다.

from sklearn.model_selection import RandomizedSearchCV

param_dist = {"n_estimators": [10, 20, 30, 50], "max_depth": [2, 3, 4, 5]}
rand = RandomizedSearchCV(RandomForestClassifier(random_state=42),
                          param_dist, n_iter=4, cv=3, random_state=42)
rand.fit(X_train[["age", "income"]], y_train)
print(rand.n_iter)
# 출력: 4
🎯 AICE: Professional 실기에서 GridSearchCV(estimator, param_grid, cv=...)의 인자 순서와 best_params_·best_score_ 속성명을 빈칸으로 냅니다. param_grid는 딕셔너리, 값은 리스트라는 구조를 손으로 쓸 수 있어야 합니다.

21.3 Pipeline — 전처리와 모델을 한 객체로

Pipeline은 여러 변환 단계와 마지막 모델을 하나로 묶습니다. JS의 함수 합성(compose)이나 Express 미들웨어 체인처럼, fit을 호출하면 각 단계를 순서대로 통과시킵니다.

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression

pipe = Pipeline([
    ("scaler", StandardScaler()),                 # 1단계: 스케일링
    ("clf", LogisticRegression(max_iter=1000)),   # 2단계: 모델
])
pipe.fit(X_train[["age", "income"]], y_train)
print(round(pipe.score(X_test[["age", "income"]], y_test), 2) >= 0.0)
# 출력: True

Pipeline의 진짜 가치는 누수 방지입니다. 교차검증 시 각 폴드의 train에만 스케일러가 fit되고 검증 폴드에는 transform만 적용됩니다 — 19장에서 손으로 지키던 순서를 자동으로 강제합니다.

21.4 ColumnTransformer — 열 종류별 전처리

실제 데이터는 숫자·범주형이 섞여 있어 열마다 다른 전처리가 필요합니다. ColumnTransformer는 "이 열들엔 스케일러, 저 열들엔 인코더"를 한 번에 지정합니다. JS로 치면 필드별로 다른 변환기를 매핑하는 것입니다.

from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder

pre = ColumnTransformer([
    ("num", StandardScaler(), ["age", "income"]),          # 숫자 열
    ("cat", OneHotEncoder(handle_unknown="ignore"), ["city"]),  # 범주 열
])
full = Pipeline([("pre", pre), ("clf", LogisticRegression(max_iter=1000))])
full.fit(X_train, y_train)   # 원본 DataFrame을 그대로 투입
print(round(full.score(X_test, y_test), 2) >= 0.0)
# 출력: True
⚠️ 함정: ColumnTransformer의 열 지정은 리스트(["age", "income"])여야 합니다. 문자열 하나("age")를 넣으면 축 오류가 납니다. 또한 지정하지 않은 열은 기본적으로 버려집니다(remainder="drop") — 유지하려면 remainder="passthrough"를 명시하세요.

21.5 튜닝 + Pipeline 결합 — 누수 없는 튜닝

GridSearchCV에 Pipeline을 통째로 넘기면, 전처리까지 포함해 교차검증이 돌아 누수가 원천 차단됩니다. Pipeline 단계 이름과 파라미터는 단계이름__파라미터(언더스코어 2개)로 지정합니다.

grid2 = GridSearchCV(full, {"clf__C": [0.1, 1.0]}, cv=3)
grid2.fit(X_train, y_train)
print("clf__C" in grid2.best_params_)
# 출력: True
🎯 AICE: clf__C의 더블 언더스코어(__) 표기는 Professional에서 자주 헷갈리는 지점입니다. 단계이름 + __ + 파라미터명 규칙을 기억하세요. 이 방식이 "전처리를 교차검증 안에서" 수행해 누수를 막는 정석입니다.

연습문제

Q1. max_depth를 3, 5, 7 중에서 3-폴드 교차검증으로 탐색하도록 빈칸을 채우세요.

from sklearn.model_selection import GridSearchCV
param_grid = {"max_depth": ____}
grid = GridSearchCV(model, param_grid, cv=____)
grid.fit(X_train, y_train)
best = grid.____
정답
param_grid = {"max_depth": [3, 5, 7]}
grid = GridSearchCV(model, param_grid, cv=3)
grid.fit(X_train, y_train)
best = grid.best_params_

값은 리스트, cv는 폴드 수, 결과는 best_params_에서 꺼냅니다.

Q2. 스케일러와 로지스틱 회귀를 순서대로 묶는 Pipeline을 만드세요.

from sklearn.pipeline import Pipeline
pipe = ____([
    ("scaler", StandardScaler()),
    ("clf", LogisticRegression()),
])
pipe.____(X_train, y_train)
정답
pipe = Pipeline([
    ("scaler", StandardScaler()),
    ("clf", LogisticRegression()),
])
pipe.fit(X_train, y_train)

(이름, 변환기) 튜플의 리스트를 넘기고, 전체를 하나의 estimator처럼 fit합니다.

이 챕터의 JS→Python 대응 한눈에 보기

JSPython비고
중첩 for 루프 파라미터 스윕GridSearchCV(model, param_grid, cv=)조합 × 교차검증
무작위 샘플링 탐색RandomizedSearchCV(..., n_iter=)넓은 공간 저비용 탐색
함수 합성 / 미들웨어 체인Pipeline([(name, step), ...])전처리+모델 단일 객체
필드별 변환 매핑ColumnTransformer([(name, tf, cols)])열 종류별 전처리
obj.step.param"step__param" (더블 언더스코어)Pipeline 파라미터 지정

Chapter 22. 모델 평가 심화와 교차검증

학습 목표
- K-Fold 교차검증으로 단일 분할보다 신뢰도 높은 성능을 추정할 수 있다.
- cross_val_score로 여러 폴드 점수를 한 번에 계산할 수 있다.
- ROC 곡선·AUC로 임계값 전반의 분류 성능을 평가할 수 있다.
- precision-recall 트레이드오프를 이해하고 임계값을 조정할 수 있다.

17장의 train_test_split은 데이터를 한 번만 나눕니다. 운 나쁜 분할이면 성능이 왜곡됩니다. 교차검증(cross-validation)은 분할을 여러 번 바꿔 평균 내어 이 불안정을 줄입니다. AUC·임계값 조정은 정확도 하나로는 안 보이는 분류기의 실체를 드러냅니다. 계산은 sklearn, 시각화는 matplotlib으로 수행합니다.

import numpy as np
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression

X, y = make_classification(n_samples=300, n_features=6, n_informative=4,
                           weights=[0.7, 0.3], random_state=42)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42)
model = LogisticRegression(max_iter=1000).fit(X_train, y_train)
print(X_test.shape)
# 출력: (90, 6)

22.1 K-Fold 교차검증

K-Fold는 데이터를 k등분해, 매번 한 조각을 검증용으로 두고 나머지로 학습하기를 k번 반복합니다. 개념적으로는 train_test_split을 위치를 바꿔 k번 돌려 평균 내는 것입니다.

from sklearn.model_selection import cross_val_score, KFold

kf = KFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(model, X, y, cv=kf, scoring="accuracy")
print(len(scores))   # 폴드마다 하나씩, 총 5개 점수
# 출력: 5

점수 배열의 평균과 표준편차를 함께 봅니다. 표준편차가 크면 데이터 분할에 민감하다는 신호입니다.

print(0.0 <= scores.mean() <= 1.0)
# 출력: True
⚠️ 함정: cross_val_score는 넘긴 모델을 내부에서 복제해 폴드마다 새로 학습합니다. 원본 model 객체는 바뀌지 않으므로, JS에서 함수에 객체를 넘겨 변형됐는지 걱정하던 것과 달리 부작용이 없습니다. 반환값은 "점수 배열"일 뿐, 학습된 모델이 아닙니다.

분류에서는 클래스 비율을 폴드마다 유지하는 StratifiedKFold가 기본입니다. 불균형 데이터에서 특히 중요합니다.

from sklearn.model_selection import StratifiedKFold

skf = StratifiedKFold(n_splits=5)
strat_scores = cross_val_score(model, X, y, cv=skf)
print(strat_scores.shape)
# 출력: (5,)
🎯 AICE: cross_val_score(model, X, y, cv=5)의 인자 순서와 반환이 "점수 배열"이라는 점, 그리고 .mean()으로 대표값을 내는 흐름이 Professional에 자주 나옵니다.

22.2 ROC 곡선과 AUC

정확도는 임계값 0.5를 고정한 한 점의 성능일 뿐입니다. ROC 곡선은 임계값을 0~1로 훑으며 (거짓양성률, 참양성률)을 그린 곡선이고, AUC는 그 아래 면적(1에 가까울수록 좋음, 0.5는 무작위)입니다. 임계값 전반의 분류력을 하나의 수로 요약합니다.

AUC 계산에는 클래스가 아니라 확률이 필요합니다. predict가 아니라 predict_proba를 씁니다.

from sklearn.metrics import roc_auc_score

proba = model.predict_proba(X_test)[:, 1]   # 양성(1) 클래스 확률
auc = roc_auc_score(y_test, proba)
print(0.0 <= auc <= 1.0)
# 출력: True

roc_curve는 곡선을 그릴 좌표(fpr, tpr)와 각 임계값을 반환합니다.

from sklearn.metrics import roc_curve

fpr, tpr, thresholds = roc_curve(y_test, proba)
print(fpr.min() == 0.0)
# 출력: True
⚠️ 함정: predict_proba는 (표본 수 × 클래스 수) 2차원 배열을 반환합니다. 양성 클래스 확률만 쓰려면 [:, 1]로 두 번째 열을 골라야 합니다. predict(0/1 라벨)를 roc_auc_score에 넣으면 곡선이 계단이 되어 AUC가 왜곡됩니다.

ROC 곡선을 그려 봅니다. matplotlib이 필요합니다(미설치 시 pip install matplotlib).

import matplotlib.pyplot as plt

plt.figure()
plt.plot(fpr, tpr, label=f"AUC = {auc:.2f}")
plt.plot([0, 1], [0, 1], "--")   # 무작위 분류기 기준선
plt.xlabel("False Positive Rate")
plt.ylabel("True Positive Rate")
plt.legend()
plt.show()

22.3 precision-recall 트레이드오프와 임계값 조정

기본 임계값은 0.5지만, 문제에 따라 조정합니다. 임계값을 낮추면 양성 판정이 늘어 재현율(recall)↑·정밀도(precision)↓, 높이면 반대입니다. 암 진단처럼 놓치면 안 되는 문제는 임계값을 낮춰 recall을 확보합니다.

from sklearn.metrics import precision_score, recall_score

pred_default = (proba >= 0.5).astype(int)   # 기본 임계값
pred_low = (proba >= 0.3).astype(int)        # 임계값 낮춤

r_default = recall_score(y_test, pred_default, zero_division=0)
r_low = recall_score(y_test, pred_low, zero_division=0)
print(r_low >= r_default)   # 임계값을 낮추면 recall은 줄지 않음
# 출력: True

정밀도와 재현율은 이렇게 서로 밀고 당깁니다. 둘의 조화평균이 f1 점수이고, 임계값을 직접 옮겨 원하는 지점을 고르는 것이 임계값 조정입니다.

p = precision_score(y_test, pred_default, zero_division=0)
r = recall_score(y_test, pred_default, zero_division=0)
print(0.0 <= p <= 1.0 and 0.0 <= r <= 1.0)
# 출력: True
🎯 AICE: (proba >= 임계값).astype(int)로 확률을 라벨로 바꾸는 관용구, 그리고 recall과 precision이 트레이드오프 관계라는 개념이 Professional 서술·빈칸형으로 나옵니다.

22.4 다중분류 평가

클래스가 3개 이상이면 precision·recall·f1을 클래스별로 구한 뒤 평균합니다. average 인자로 평균 방식을 정합니다: "macro"(클래스 단순 평균), "weighted"(표본 수 가중).

from sklearn.metrics import f1_score

X3, y3 = make_classification(n_samples=200, n_features=6, n_informative=4,
                             n_classes=3, random_state=42)
m3 = LogisticRegression(max_iter=1000).fit(X3, y3)
pred3 = m3.predict(X3)
macro_f1 = f1_score(y3, pred3, average="macro")   # 다중분류엔 average 필수
print(0.0 <= macro_f1 <= 1.0)
# 출력: True
🎯 AICE (출제 이력 불확실): 다중분류 세부 평가(average 옵션 선택 등)는 AICE 공개 범위에 명시가 적어 회차별 출제 편차가 있을 수 있습니다. 이진분류의 accuracy/precision/recall/f1/AUC를 먼저 확실히 하고, 다중분류는 "average="macro"를 지정한다"는 개념 수준으로 대비하는 편이 안전합니다.

연습문제

Q1. 5-폴드 교차검증 정확도의 평균을 구하도록 빈칸을 채우세요.

from sklearn.model_selection import cross_val_score
scores = cross_val_score(model, X, y, cv=____, scoring="accuracy")
print(scores.____())
정답
scores = cross_val_score(model, X, y, cv=5, scoring="accuracy")
print(scores.mean())

cv는 폴드 수, 결과 배열에 .mean()으로 대표값을 냅니다.

Q2. 양성 클래스 확률로 AUC를 계산하도록 빈칸을 채우세요.

from sklearn.metrics import roc_auc_score
proba = model.____(X_test)[:, 1]
auc = roc_auc_score(y_test, ____)
정답
proba = model.predict_proba(X_test)[:, 1]
auc = roc_auc_score(y_test, proba)

predict_proba로 확률을 얻고 [:, 1]로 양성 클래스 열을 골라 AUC에 넣습니다.

이 챕터의 JS→Python 대응 한눈에 보기

JSPython비고
split을 여러 번 반복해 평균cross_val_score(model, X, y, cv=)반환은 점수 배열
객체 넘겨도 원본 불변cross_val_score는 모델 복제 학습부작용 없음
임계값별 성능 곡선roc_curve / roc_auc_score확률(predict_proba) 필요
배열 열 선택 arr.map(r=>r[1])predict_proba(X)[:, 1]양성 클래스 확률
판정 경계 조정(proba >= t).astype(int)임계값 튜닝

Chapter 23. 딥러닝 심화: CNN·RNN·시계열

학습 목표
- CNN(합성곱 신경망)의 Conv2D·Pooling 구조와 이미지 데이터 형태를 이해한다.
- RNN/LSTM으로 시퀀스 데이터를 처리하는 모델을 구성할 수 있다.
- 시계열 데이터를 윈도우(window)로 잘라 지도학습 형태로 변환할 수 있다.
- EarlyStopping·ModelCheckpoint 콜백을 학습에 활용할 수 있다.

18장의 Sequential+Dense는 정형 데이터(표)를 다뤘습니다. 이미지·텍스트·시계열은 공간·순서 구조를 가지므로 전용 층이 필요합니다. 이미지엔 CNN, 순서 데이터엔 RNN/LSTM입니다.

⚠️ 실행 환경 안내: 이 장의 코드는 TensorFlow/Keras가 필요합니다. 미설치 시 다음으로 설치하세요.
```bash
pip install tensorflow
```
딥러닝 학습은 무거우므로 이 장 예제는 입력 크기·에폭을 개념 확인용으로 최소화했습니다(실무에선 더 큰 데이터·에폭). 층을 쌓는 발상은 18장과 동일하니, 새 층의 역할과 인자에 집중하세요.
🎯 AICE (출제 이력 불확실): CNN·RNN의 AICE Professional 실기 출제 비중은 공개 정보가 적어 확실하지 않습니다. 다만 딥러닝 심화는 Professional의 성격상 다뤄질 가능성이 높아, 구조를 손으로 구성할 수 있는 수준까지 익혀두는 것을 권합니다.

23.1 CNN — 이미지용 합성곱 신경망

정형 데이터는 Dense로 충분하지만, 이미지는 인접 픽셀의 공간 패턴(모서리·질감)이 중요합니다. Conv2D는 작은 필터를 이미지 위로 훑어 이런 국소 패턴을 뽑고, MaxPooling2D는 크기를 줄여 계산량과 위치 민감도를 낮춥니다.

import tensorflow as tf
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense

tf.random.set_seed(42)
cnn = Sequential([
    Conv2D(32, (3, 3), activation="relu", input_shape=(28, 28, 1)),
    MaxPooling2D((2, 2)),
    Conv2D(64, (3, 3), activation="relu"),
    MaxPooling2D((2, 2)),
    Flatten(),                              # 2D 특성맵 → 1D 벡터
    Dense(64, activation="relu"),
    Dense(10, activation="softmax"),        # 10개 클래스 분류
])
cnn.compile(optimizer="adam",
            loss="sparse_categorical_crossentropy",
            metrics=["accuracy"])
print(cnn.output_shape)   # 마지막 층 출력 형태
# 출력: (None, 10)

핵심은 마지막 두 층입니다. Flatten으로 합성곱이 만든 2D 특성맵을 펼쳐야 Dense에 연결됩니다.

23.2 이미지 데이터의 형태

이미지 데이터는 4차원 텐서 (표본 수, 높이, 너비, 채널)입니다. 흑백은 채널 1, 컬러(RGB)는 3입니다. 픽셀값(0~255)은 학습 안정을 위해 255로 나눠 0~1로 정규화합니다.

import numpy as np

np.random.seed(42)
# 예: 흑백 28×28 이미지 100장
images = np.random.randint(0, 256, size=(100, 28, 28, 1))
images = images.astype("float32") / 255.0   # 0~1 정규화
print(images.shape)
# 출력: (100, 28, 28, 1)
⚠️ 함정: Conv2D의 input_shape에는 표본 수를 넣지 않습니다. (28, 28, 1)처럼 한 장의 형태만 지정하고, 표본 축은 Keras가 배치 처리 시 자동으로 붙입니다. JS에서 배열 전체 길이를 함께 넘기던 습관과 다릅니다.

23.3 RNN/LSTM — 순서가 있는 데이터

문장·주가처럼 순서가 의미를 갖는 데이터는 RNN 계열로 처리합니다. 기본 RNN은 긴 시퀀스에서 앞부분을 잊는 문제가 있어, 이를 개선한 LSTM을 주로 씁니다. 입력 형태는 (표본 수, 타임스텝, 특성 수) 3차원입니다.

from tensorflow.keras.layers import LSTM

timesteps, features = 10, 1
model = Sequential([
    LSTM(64, input_shape=(timesteps, features)),   # 순서 정보 처리
    Dense(1),                                       # 회귀 출력(다음 값 예측)
])
model.compile(optimizer="adam", loss="mse")
print(model.output_shape)
# 출력: (None, 1)

텍스트 분류라면 LSTM 앞에 Embedding 층을 두어 단어 인덱스를 밀집 벡터로 바꿉니다(24장 참조).

from tensorflow.keras.layers import Embedding

text_model = Sequential([
    Embedding(input_dim=10000, output_dim=32),   # 어휘 1만 → 32차원 벡터
    LSTM(64),
    Dense(1, activation="sigmoid"),               # 이진 분류
])
print(len(text_model.layers))
# 출력: 3

23.4 시계열 데이터 준비 — 윈도우 슬라이싱

시계열은 원본이 1차원 수열이라, 지도학습에 넣으려면 윈도우로 잘라야 합니다. "직전 n개 값 → 다음 값"을 (X, y) 쌍으로 만드는 것입니다. 이 변환은 순수 numpy로, 발상은 JS 배열 슬라이딩 윈도우와 같습니다.

def make_windows(series, window):
    X, y = [], []
    for i in range(len(series) - window):
        X.append(series[i:i + window])   # 직전 window개
        y.append(series[i + window])     # 바로 다음 값
    return np.array(X), np.array(y)

series = np.sin(np.linspace(0, 20, 200))
X, y = make_windows(series, window=10)
X = X.reshape(X.shape[0], X.shape[1], 1)   # (표본, 타임스텝, 특성)로 reshape
print(X.shape)
# 출력: (190, 10, 1)
⚠️ 함정: LSTM 입력은 반드시 3차원이어야 합니다. make_windows가 만든 2차원 (표본, 타임스텝)을 reshape(..., ..., 1)로 특성 축을 추가해야 합니다. 이 축을 빠뜨리면 expected 3 dimensions 에러가 납니다.

23.5 콜백 — 학습 제어

18장에서 본 EarlyStopping·ModelCheckpoint는 심화 학습에서 더 중요합니다. 긴 학습에서 과적합 직전에 멈추고(EarlyStopping), 최고 성능 시점을 저장(ModelCheckpoint)합니다.

import os, tempfile
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint

ckpt_path = os.path.join(tempfile.gettempdir(), "best_model.keras")
callbacks = [
    EarlyStopping(monitor="val_loss", patience=3,
                  restore_best_weights=True),          # 개선 없으면 조기 중단
    ModelCheckpoint(ckpt_path, monitor="val_loss",
                    save_best_only=True),               # 최고 성능만 저장
]

history = model.fit(
    X, y,
    epochs=3, batch_size=32,   # 시연용 3에폭 (실무에선 크게, EarlyStopping이 중단 담당)
    validation_split=0.2,      # train의 20%를 검증에
    callbacks=callbacks,
    verbose=0,
)
print(len(history.history["loss"]) <= 3)
# 출력: True
🎯 AICE: EarlyStopping(monitor="val_loss", patience=...)와 fit(..., callbacks=[...])의 결합은 Professional 딥러닝 문항의 단골입니다. monitor는 감시할 지표, patience는 참을 에폭 수, restore_best_weights=True로 최적 가중치를 복원한다는 흐름을 외우세요.

연습문제

Q1. 첫 합성곱 층에 필터 32개, 3×3 커널, 흑백 28×28 입력을 지정하세요.

model = Sequential([
    Conv2D(____, (____, ____), activation="relu", input_shape=(28, 28, ____)),
    MaxPooling2D((2, 2)),
    Flatten(),
    Dense(10, activation="softmax"),
])
정답
model = Sequential([
    Conv2D(32, (3, 3), activation="relu", input_shape=(28, 28, 1)),
    MaxPooling2D((2, 2)),
    Flatten(),
    Dense(10, activation="softmax"),
])

필터 수 32, 커널 (3,3), 흑백 채널 1. input_shape에 표본 수는 넣지 않습니다.

Q2. 검증 손실이 3에폭 개선되지 않으면 학습을 멈추는 콜백을 만드세요.

from tensorflow.keras.callbacks import EarlyStopping
es = EarlyStopping(monitor="____", patience=____, restore_best_weights=True)
model.fit(X, y, epochs=50, callbacks=[____])
정답
es = EarlyStopping(monitor="val_loss", patience=3, restore_best_weights=True)
model.fit(X, y, epochs=50, callbacks=[es])

monitor="val_loss", patience=3, 콜백은 callbacks 리스트에 담습니다.

이 챕터의 JS→Python 대응 한눈에 보기

JSPython(Keras)비고
층 함수 합성Sequential([Conv2D(...), ...])18장 Dense 스택의 확장
배열 전체 길이 전달input_shape=(28,28,1) (표본 축 제외)배치 축은 자동
슬라이딩 윈도우series[i:i+window] + reshape시계열 → (표본,타임스텝,특성)
조기 종료 조건문EarlyStopping(patience=...)콜백으로 학습 제어
단어 → 벡터 매핑Embedding(input_dim, output_dim)24장 텍스트와 연결

Chapter 24. 텍스트 데이터 처리 기초

학습 목표
- 토큰화·정규화·불용어 제거의 역할을 이해한다.
- CountVectorizer로 텍스트를 단어 빈도 행렬(BoW)로 변환할 수 있다.
- TfidfVectorizer로 단어 중요도를 반영한 특성을 만들 수 있다.
- 벡터라이저와 분류기를 Pipeline으로 묶어 텍스트 분류를 수행할 수 있다.

머신러닝 모델은 숫자만 먹습니다. 텍스트를 넣으려면 수치 특성으로 바꿔야 합니다. 이 장은 그 표준 도구인 CountVectorizer·TfidfVectorizer(sklearn)를 다루고, 마지막에 딥러닝 임베딩(keras)을 맛봅니다.

corpus = [
    "Python is great for data science",
    "JavaScript is great for web",
    "Data science uses python and pandas",
    "Web development uses javascript",
]
labels = [1, 0, 1, 0]   # 1 = 데이터과학 글, 0 = 웹 글

tokens = corpus[0].lower().split()   # 가장 단순한 토큰화
print(tokens[:3])
# 출력: ['python', 'is', 'great']

24.1 토큰화·정규화·불용어

텍스트를 숫자로 바꾸기 전 세 가지를 합니다. 토큰화(문장을 단어로 쪼개기), 정규화(대소문자 통일·구두점 제거), 불용어 제거(is, for, the처럼 의미가 옅은 단어 버리기). 위 lower().split()은 토큰화+정규화를 최소한으로 흉내 낸 것이지만, 실무에서는 벡터라이저가 이를 내장 처리합니다.

JavaScriptPython
``"Hello World".toLowerCase().split(" ")````"Hello World".lower().split()``

24.2 CountVectorizer — 단어 빈도 행렬(BoW)

CountVectorizer는 말뭉치의 모든 단어로 어휘 사전을 만들고, 각 문서를 "단어별 등장 횟수" 벡터로 바꿉니다. 이 표현을 BoW(Bag of Words)라 합니다. JS로 치면 각 문서를 {단어: 횟수} 딕셔너리로 만든 뒤 공통 열에 정렬한 것입니다.

from sklearn.feature_extraction.text import CountVectorizer

cv = CountVectorizer()
X_count = cv.fit_transform(corpus)   # (문서 수 × 어휘 수) 희소 행렬
print(X_count.shape[0])
# 출력: 4

학습된 어휘는 vocabulary_(단어→열 인덱스 딕셔너리)에 담깁니다. CountVectorizer는 기본으로 소문자화하므로 "Python"도 "python"으로 저장됩니다.

print("python" in cv.vocabulary_)
# 출력: True
⚠️ 함정: fit_transform의 결과는 일반 배열이 아니라 희소 행렬(sparse matrix)입니다. 어휘가 수만 개여도 대부분 0이라 메모리를 아끼는 구조입니다. 내용을 눈으로 보려면 .toarray()로 밀집 배열로 바꿔야 합니다 — JS 배열처럼 바로 인덱싱되지 않습니다.

stop_words="english"를 주면 영어 불용어가 자동 제거됩니다.

cv_sw = CountVectorizer(stop_words="english")
cv_sw.fit(corpus)
print("is" in cv_sw.get_feature_names_out())   # 불용어 'is'는 제거됨
# 출력: False

24.3 TfidfVectorizer — 단어 중요도 반영

단순 빈도는 흔한 단어를 과대평가합니다. TF-IDF는 "한 문서에서 자주 나오되(TF) 전체 문서에서는 드문(IDF)" 단어에 높은 가중치를 줍니다. 즉 그 문서를 특징짓는 단어를 부각합니다. 사용법은 CountVectorizer와 동일합니다.

from sklearn.feature_extraction.text import TfidfVectorizer

tfidf = TfidfVectorizer()
X_tfidf = tfidf.fit_transform(corpus)
print(X_tfidf.shape[0])
# 출력: 4
🎯 AICE: CountVectorizer와 TfidfVectorizer는 이름만 다르고 fit_transform/get_feature_names_out 인터페이스가 같습니다. "빈도만 vs 중요도 가중"의 차이와, 둘 다 sklearn 벡터라이저라는 점을 함께 기억하세요.

24.4 텍스트 분류 파이프라인

벡터라이저도 estimator이므로 21장의 Pipeline에 그대로 들어갑니다. "텍스트 → 벡터화 → 분류기"를 한 객체로 묶으면, 원문 리스트를 바로 fit·predict할 수 있습니다. 텍스트 분류에는 MultinomialNB(나이브 베이즈)가 가볍고 강력합니다.

from sklearn.pipeline import Pipeline
from sklearn.naive_bayes import MultinomialNB

pipe = Pipeline([
    ("tfidf", TfidfVectorizer(stop_words="english")),
    ("clf", MultinomialNB()),
])
pipe.fit(corpus, labels)
pred = pipe.predict(["python pandas data analysis"])   # 원문을 바로 투입
print(pred[0] in (0, 1))
# 출력: True
⚠️ 함정: Pipeline에 벡터라이저를 넣으면, predict에 넘기는 것도 벡터가 아니라 원문 문자열 리스트입니다. 직접 fit_transform으로 벡터를 만든 뒤 그 벡터를 다시 넘기려다 이중 변환하는 실수를 피하세요. Pipeline이 변환을 내부에서 처리합니다.

24.5 임베딩 개요

BoW·TF-IDF는 단어를 독립된 열로만 봐서 "python"과 "javascript"의 의미적 유사성을 모릅니다. 임베딩(embedding)은 단어를 의미가 담긴 밀집 벡터로 학습해 이 한계를 넘습니다(딥러닝, 23장 참조). Embedding 층은 단어 인덱스 시퀀스를 받아 각 단어를 밀집 벡터로 바꿉니다.

import numpy as np
import tensorflow as tf
from tensorflow.keras.layers import Embedding

tf.random.set_seed(42)
# 희소한 BoW 대신, 단어 인덱스를 8차원 밀집 벡터로 학습
emb = Embedding(input_dim=100, output_dim=8)   # 어휘 100, 벡터 8차원
sample = np.array([[3, 7, 1]])                  # 단어 인덱스 시퀀스 1개
vectors = emb(sample)
print(vectors.shape)   # (문서 1, 단어 3, 임베딩 8)
# 출력: (1, 3, 8)
🎯 AICE (출제 이력 불확실): 임베딩·딥러닝 기반 텍스트 처리는 AICE 공개 범위에 명시가 적어 출제 여부가 확실하지 않습니다. 시험 대비 우선순위는 실행 가능한 CountVectorizer·TfidfVectorizer와 텍스트 분류 파이프라인에 두고, 임베딩은 개념 수준으로 이해해 두면 충분합니다.

연습문제

Q1. 영어 불용어를 제거하는 TF-IDF 벡터라이저로 말뭉치를 변환하세요.

from sklearn.feature_extraction.text import TfidfVectorizer
vec = TfidfVectorizer(____="english")
X = vec.____(corpus)
정답
vec = TfidfVectorizer(stop_words="english")
X = vec.fit_transform(corpus)

stop_words="english"로 불용어를 제거하고 fit_transform으로 벡터화합니다.

Q2. TF-IDF 벡터화와 나이브 베이즈 분류기를 Pipeline으로 묶으세요.

from sklearn.pipeline import Pipeline
from sklearn.naive_bayes import MultinomialNB
pipe = Pipeline([
    ("tfidf", ____()),
    ("clf", ____()),
])
pipe.fit(corpus, labels)
정답
pipe = Pipeline([
    ("tfidf", TfidfVectorizer()),
    ("clf", MultinomialNB()),
])
pipe.fit(corpus, labels)

벡터라이저와 분류기를 순서대로 묶으면 원문을 바로 학습할 수 있습니다.

이 챕터의 JS→Python 대응 한눈에 보기

JSPython비고
str.toLowerCase().split(" ")str.lower().split()토큰화+정규화
{단어: 횟수} 딕셔너리CountVectorizer (BoW)단어 빈도 행렬
가중치 부여 맵TfidfVectorizer흔한 단어 억제, 특징 단어 부각
조밀 배열희소 행렬 (.toarray()로 변환)메모리 효율
변환+분류 합성Pipeline([vectorizer, clf])원문을 바로 fit/predict

Appendix A. JS → Python 치트시트

전 챕터의 "한눈에 보기" 표를 한곳에 모은 대조표입니다. 시험 직전 훑어보기·실무 참조용으로 쓰세요. 표 안 코드는 3줄 이하 스니펫이며, 자세한 설명·함정은 각 챕터를 참조합니다.

A.1 실행 환경 (1장)

JSPython비고
node app.jspython app.py인터프리터 직접 실행
npm install Xpip install X가상환경 활성화 후
node_modules.venv (venv)자동 격리 아님
package.json 의존성requirements.txtpip freeze로 생성
require.main === moduleif __name__ == "__main__":스크립트 진입점

A.2 변수·타입·연산자 (2장) — 함정 밀집 구간

JSPython비고
let / const그냥 할당 / 대문자 관례const 없음
=====값 비교
동일 참조 비교is정체성 비교
x == nullx is NoneNone은 싱글턴
Math.floor(a/b)a // b정수 나눗셈
[]/{} truthy[]/{} falsy⚠️ 분기 반대

A.3 문자열·포매팅 (3장)

JSPython비고
` ${x} `f"{x}"따옴표 + f
x.toFixed(2)f"{x:.2f}"포맷 스펙
n.toLocaleString()f"{n:,}"천단위
arr.join("-")"-".join(arr)구분자가 주어
s.trim()s.strip()양끝 공백
s.replaceAll(a,b)s.replace(a,b)기본 전체 치환

A.4 컬렉션·슬라이싱 (4장)

JSPython비고
arr.push(x)lst.append(x)⚠️ push 없음
arr.lengthlen(lst)함수 호출
arr.slice(1,4)lst[1:4]stop 미포함
[...arr].reverse()lst[::-1]새 리스트
obj["k"](→undefined)d.get("k")안전 접근
new Set([...]){...} / set()빈 set은 set()
(없음)(x, y) 튜플불변·언패킹
includes/hasin공통 연산

A.5 제어 흐름 (5장)

JSPython비고
if(...){}if ...: + 들여쓰기중괄호 없음
else ifelif
for (const x of arr)for x in arr:값 순회
for(let i=0;...)for i in range(n):
arr.forEach((v,i)=>)enumerate(arr)(인덱스, 값)
(없음)zip(a, b)시퀀스 병렬
cond ? a : ba if cond else b⚠️ 어순 뒤집힘
n++n += 1증감 없음

A.6 컴프리헨션 (6장)

JSPython비고
arr.map(f)[f(x) for x in arr]표현식 직접
arr.filter(p)[x for x in arr if p(x)]뒤쪽 if
arr.filter().map()[f(x) for x in arr if p(x)]한 줄
Object.fromEntries{k: v for ...}dict 컴프리헨션
(지연 평가 없음)(f(x) for x in arr)제너레이터

A.7 함수 (7장)

JSPython비고
(a,b)=>a+bdef f(a,b): return a+b암묵 return 없음
x=>x*xlambda x: x*x단일 표현식만
rest ...args*args (튜플)위치 인자 모으기
(이름 인자 모으기 없음)**kwargs (dict)키워드 모으기
spread f(...arr)f(*arr) / f(**d)언패킹 호출
(함정 없음)def f(x=[]) 공유⚠️ None 기본값 관용구

A.8 스코프·클로저 (8장)

JSPython비고
블록 스코프함수 스코프(LEGB)if/for는 스코프 아님
바깥 변수 그냥 수정global/nonlocal재할당엔 선언 필수
let 루프 새 바인딩늦은 바인딩 함정⚠️ lambda x=x: 트릭

A.9 클래스 (9장)

JSPython비고
constructor()__init__(self, ...)생성자 dunder
this (암묵)self (명시)⚠️ 첫 인자 항상
new C()C()new 없음
toString()__repr__문자열 표현
extends/super()(Parent)/super().__init__()상속

A.10 모듈·에러·이터레이터 (10~12장)

JSPython비고
import { x } from 'm'from m import x어순 반대
import { x as y }from m import x as y별칭
try{}catch(e){}try: ... except E as e:타입 명시
(없음)else: 절예외 없을 때
throw new Error()raise ValueError()예외 발생
it.next().valuenext(it)다음 값
function*(){yield}def f(): yield제너레이터

A.11 NumPy 핵심 (13장)

JSPython (NumPy)비고
new Float64Array([...])np.array([...])벡터 배열
arr.lengtharr.shape튜플(다차원)
mat[i][j]mat[i, j]쉼표 접근
arr.map(f)arr 통째 연산루프 불필요
arr.filter(x=>x>10)arr[arr > 10]불리언 인덱싱
&& / `\\`& / `\` (괄호 필수)and/or 아님
arr.reduce(+)arr.sum(axis=0)axis로 방향

A.12 pandas 핵심 (14~15장)

JSPython (pandas)비고
행 객체 배열pd.DataFrame({열:[...]})열 딕셔너리 생성
arr[i]df.iloc[i]정수 위치
(대응 없음)df.loc[라벨, 열]라벨 기반
arr.filter(p=>p.x>0)df[df["x"] > 0]불리언 인덱싱
filter(A && B)df[(A) & (B)]괄호+& 필수
x == null 체크df.isnull()==np.nan은 항상 False
x ?? 기본df["c"].fillna(값)결측 대치
Number(x)df["c"].astype("int64")타입 변환
groupby 수동df.groupby("k")["v"].mean()GROUP BY
수동 조인pd.merge(a, b, on="k")JOIN

A.13 전처리 인코딩·스케일링 (15장, 19장)

목적Python (sklearn/pandas)비고
원-핫 인코딩(입력 X)pd.get_dummies(df, columns=[...])범주 → 이진 열
라벨 인코딩(목표 y)LabelEncoder().fit_transform(y)범주 → 정수
표준화StandardScaler().fit_transform(X)평균0 분산1
정규화MinMaxScaler().fit_transform(X)0~1 범위
결측 대치SimpleImputer(strategy="mean")평균/최빈
train/test 분리 적용train fit_transform, test transform⚠️ 누수 방지

A.14 scikit-learn 모델링 (17장, 20~22장)

단계Python (scikit-learn)비고
데이터 분리train_test_split(X, y, test_size=0.2, random_state=42)반환 순서 고정
회귀LinearRegression().fit/.predict
분류LogisticRegression() / DecisionTreeClassifier(max_depth=)
앙상블RandomForestClassifier(n_estimators=) / GradientBoostingClassifier(learning_rate=)Regressor 짝 존재
학습/예측model.fit(X_train, y_train) → model.predict(X_test)
회귀 지표r2_score / root_mean_squared_error / mean_absolute_error⚠️(정답, 예측) 순
분류 지표accuracy_score / f1_score / confusion_matrix⚠️(정답, 예측) 순
교차검증cross_val_score(model, X, y, cv=5)점수 배열 반환
튜닝GridSearchCV(model, param_grid, cv=5)best_params_/best_score_
파이프라인Pipeline([("prep", ...), ("clf", ...)])파라미터 clf__C

A.15 Keras 딥러닝 (18장, 23장)

단계Python (Keras)비고
모델keras.Sequential([...])순차 스택
완전연결층layers.Dense(뉴런, activation="relu")은닉층
입력keras.Input(shape=(특성수,))배치 제외
컴파일model.compile(optimizer="adam", loss=, metrics=["accuracy"])loss↔활성화 짝
학습model.fit(X, y, epochs=, batch_size=, validation_data=)history 반환
콜백EarlyStopping(monitor="val_loss", patience=)callbacks=[...]
평가model.evaluate(X_test, y_test)[loss, metrics]
CNN/RNNConv2D / LSTM / Embedding23장

A.16 시각화·텍스트 (16장, 24장)

목적Python비고
범주 빈도sns.countplot(data=df, x="col")EDA
수치 분포sns.histplot(data=df, x="col", bins=)
이상치sns.boxplot(data=df, x=, y=)사분위
상관관계sns.heatmap(df.corr(), annot=True)
학습곡선plt.plot(history.history["loss"]) + plt.legend()범례 짝
BoWCountVectorizer().fit_transform(docs)빈도 행렬
TF-IDFTfidfVectorizer().fit_transform(docs)중요도 가중

A.17 JS 습관성 함정 총정리

시험·실무에서 조용히 틀리기 쉬운 핵심 함정만 추린 목록입니다.

함정잘못된 JS 습관Python 정답
truthinessif (arr)if arr is not None: (존재 확인)
None 비교x == Nonex is None
정수 나눗셈Math.floor(a/b)a // b
리스트 추가arr.push(x)lst.append(x)
삼항 어순cond ? a : ba if cond else b
self 누락method() { this... }def method(self):
가변 기본값def f(x=[])def f(x=None): 내부 생성
pandas 논리df[A and B]df[(A) & (B)]
지표 인자 순서f1_score(pred, y)f1_score(y_test, pred)
NaN 비교x == np.nandf["c"].isnull()

Appendix B. AICE 실기 팁과 자주 하는 실수

AICE 실기는 빈 셀에 코드를 채워 오류 없이 실행시키는 형태입니다. 자동완성에 의존하면 시험장에서 무너지므로, API 시그니처를 손으로 쓸 수 있어야 합니다. 이 부록은 각 챕터의 🎯 AICE 콜아웃과 시험 범위 참조를 트랙별로 취합했습니다.

B.1 시험 트랙 개요

트랙대상범위이 책의 커버
Associate실무자·개발자EDA→전처리→분리→모델링→평가 파이프라인Part 1 + Part 2 (1~18장)
ProfessionalAI 개발자Associate + 특성공학·앙상블·튜닝·교차검증·CNN/RNN·텍스트전체 (1~24장)

두 트랙 모두 Jupyter + Python 실기입니다. Associate를 통과한 뒤 Part 3를 얹으면 Professional로 이어집니다.

B.2 공통 실기 팁 (JS 개발자용)

Restart & Run All로 재현성 확인. Jupyter 셀은 실행한 순서대로 상태가 쌓입니다(1장). 위→아래 순서와 실제 실행 순서가 다르면 채점 시 오류가 납니다. 제출 전 반드시 커널을 재시작하고 처음부터 전부 실행하세요.

첫 셀 import는 통째로 암기. 실기 첫 셀은 거의 항상 아래로 시작합니다. 별칭을 틀리면 이후 모든 코드가 연쇄로 무너집니다(10장).

import numpy as np
import pandas as pd
print(np.__name__, pd.__name__)
# 출력: numpy pandas

shape를 습관적으로 확인. 모델 입력 전 X.shape로 (샘플 수, 특성 수)를 눈으로 확인하는 것이 shape 불일치 에러를 막는 가장 빠른 길입니다(13장).

포맷 스펙으로 답 제출. 성능 출력은 print(f"정확도: {acc:.2f}")처럼 자릿수를 고정합니다(3장).

B.3 JS 개발자 습관성 오타 체크리스트

시험장에서 무의식적으로 저지르는 JS 습관입니다. 제출 전 이 목록을 훑으세요.

#JS 습관Python 정답근거
1arr.push(x)lst.append(x)4장
2x == Nonex is None2장
3if (arr)로 존재 확인if arr is not None:2장
4cond ? a : ba if cond else b5장
5self 누락def method(self):9장
6df[A and B]df[(A) & (B)]14장
7f1_score(pred, y)f1_score(y_test, pred)17장
8탭·스페이스 혼용스페이스 4칸 통일5장

B.4 Associate 트랙 팁

Associate는 정형화된 파이프라인을 그대로 따라갑니다. 순서와 대표 API를 통째로 외우면 절반은 풉니다.

파이프라인 6단계 시그니처.

# 1. EDA
df.head(); df.info(); df.describe()
# 2. 전처리
df.isnull().sum(); df["c"].fillna(값); pd.get_dummies(df, columns=[...])
# 3. 분리
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 4. 모델링
model = LogisticRegression(); model.fit(X_train, y_train)
# 5. 예측·평가
pred = model.predict(X_test); accuracy_score(y_test, pred)
# 6. 딥러닝
model.compile(optimizer="adam", loss="...", metrics=["accuracy"])

B.5 Professional 트랙 팁

Professional은 Associate 파이프라인을 심화·자동화합니다. 코딩 비중이 높고, 데이터 누수 방지가 핵심 채점 포인트입니다.

B.6 트랙별 시간 배분 가이드

단계AssociateProfessional
EDA·전처리40%30%
모델링·학습35%35%
평가·튜닝·시각화15%25% (튜닝·교차검증 비중↑)
검토(Restart & Run All)10%10%

전처리에서 막히면 뒤 단계 전체가 밀립니다. 결측·인코딩·스케일링 시그니처를 반사적으로 쓸 수 있을 때까지 손에 익히는 것이 최우선입니다.

ℹ️ AICE 공식 출제 범위는 회차별 편차가 있습니다. 최신 정보는 aice.study 공식 사이트에서 확인하세요. 특히 Professional 상세 범위(불균형 데이터·다중분류 평가·임베딩)와 XGBoost/LightGBM 출제 이력은 갱신될 수 있습니다.

Appendix C. Associate vs Professional 로드맵

두 트랙의 범위를 비교하고, 챕터 의존 관계와 트랙별 최단 학습 경로를 제시합니다. 시간이 한정된 JS 개발자가 "무엇을 먼저, 어디까지" 볼지 결정하는 지도입니다.

C.1 두 트랙 범위 비교

영역AssociateProfessional챕터
Python 문법 기초필수필수ch01~12
NumPy·pandas필수필수ch13~15
시각화(EDA)중하ch16
sklearn 기본 모델링필수필수ch17
Keras 기본필수필수ch18
특성 공학 심화중필수ch19
앙상블 모델중필수ch20
튜닝·Pipeline하필수ch21
교차검증·평가 심화중필수ch22
CNN·RNN·시계열하필수ch23
텍스트 처리하중ch24

핵심 차이는 Part 3(ch19~24)입니다. Associate는 "동작하는 파이프라인 한 벌"이면 충분하지만, Professional은 그 파이프라인을 자동화·심화·정밀 평가하는 역량까지 요구합니다.

C.2 챕터 의존 그래프

Part 1은 대체로 선형이고, Part 2·3에서 NumPy·pandas가 하위 챕터의 토대가 됩니다. NumPy(13)와 pandas(14, 15)를 먼저 확정하지 않으면 이후 예제가 불안정합니다.

[Part 1 — 문법]  01 → 02 → 03 → 04 → 05 → 06 → 07 → 08 → 09 → 10 → 11 → 12
                                  │
                                  └──(04 슬라이싱, 06 벡터화 사고)──┐
                                                                   ▼
[Part 2 — Associate]        13(NumPy) → 14(pandas) → 15(전처리) → ┬→ 16(시각화)
                                                                   └→ 17(sklearn)
                                                                        │
                          ┌──────────────┬──────────────┬─────────────┘
                          ▼              ▼              ▼
[Part 3 — Professional]  18(Keras)*    19(특성공학)   20(앙상블)   22(평가심화)
                          │                            │
                          ▼                            ▼
                         23(CNN·RNN)                  21(튜닝) → 24(텍스트)
* 18(Keras)은 Part 2(Associate) 챕터입니다. 그래프에서는 하위 의존(18 → 23)을 한눈에 보이려 Part 3 행에 배치했을 뿐이며, 소속 자체는 Part 2입니다.

C.3 트랙별 최단 학습 경로

Associate 최단 경로 (ch01~18)

목표가 Associate뿐이라면 Part 3는 건너뛰어도 됩니다.

문법 토대   : 01 → 02 → 04 → 05 → 06   (03·07~12는 필요 시 참조)
데이터 핸들 : 13 → 14 → 15
모델링·평가 : 17 → 18
EDA 시각화  : 16 (병행)

문법에서는 함정 챕터(02 truthiness·is None, 04 슬라이싱·컬렉션, 05 삼항·블록스코프, 06 컴프리헨션)에 시간을 집중하고, JS와 거의 같은 부분(03·10·11)은 빠르게 통과합니다.

Professional 최단 경로 (전체)

Associate 경로를 마친 뒤 Part 3를 얹습니다.

(Associate 경로 완료 전제)
특성·모델 심화 : 07 → 08 → 09 (문법 보강) → 19 → 20
자동화·평가    : 21 → 22
딥러닝·텍스트  : 23 → 24

Professional은 코딩 비중이 높아 07(인자 체계)·08(스코프)·09(클래스) 문법 보강이 실전에서 되살아납니다. fit_transform/transform 분리, Pipeline __ 표기, 콜백 등 "누수 없는 자동화"가 채점 핵심입니다.

C.4 학습 순서 요약

단계우선순위챕터트랙
1문법 함정 집중02, 04, 05, 06공통
2데이터 핸들링13, 14, 15공통
3기본 모델링17, 18공통
4EDA 시각화16Associate 중심
5문법 보강07, 08, 09Professional
6심화 모델링19, 20, 21, 22Professional
7딥러닝·텍스트23, 24Professional
Appendix A(치트시트)와 B(실기 팁)를 이 경로와 함께 반복 참조하면, 문법 → 파이프라인 → 시험 대비가 한 흐름으로 이어집니다.