목록으로

프로그래밍 · Python

Python 완전정복 시리즈 #28: scikit-learn 완벽 이해하기

BeanCon
Python scikit-learn으로 머신러닝 예측 모델을 만드는 방법을 설명하는 대표 이미지

Python 완전정복 시리즈 28편입니다. 대표 머신러닝 라이브러리 scikit-learn을 이용해 데이터에서 규칙을 찾고 예측 모델을 만드는 과정을 정리했습니다. 데이터 분리, X와 y, 회귀와 분류, 모델 평가, 혼동행렬, 정밀도와 재현율, F1과 ROC AUC, 전처리, 스케일링, 인코딩, 결측값 처리, ColumnTransformer, Pipeline, 데이터 누수, 교차검증, 하이퍼파라미터 튜닝, 모델 저장과 고객 이탈 예측 미니 프로젝트까지 다룹니다.

목차

메타 설명
머신러닝은 어떻게 과거 데이터에서 규칙을 찾아 새로운 결과를 예측할까요? Python의 대표 머신러닝 라이브러리 scikit-learn을 이용해 데이터 분리, 전처리, 회귀와 분류, 모델 학습, 성능 평가, 교차검증, 파이프라인, 하이퍼파라미터 튜닝, 모델 저장까지 실습 중심으로 알아봅니다.

지난 시간에는 SciPy를 이용해 복잡한 과학 계산 문제를 해결했습니다.

함수의 최솟값을 찾고, 방정식의 해를 계산하고, 곡선을 데이터에 맞췄으며, 센서 신호의 잡음을 제거했습니다.

그런데 실무에서는 한 단계 더 흥미로운 질문이 등장합니다.

이 고객은 서비스를 해지할까?

이 집의 적정 가격은 얼마일까?

이 메일은 스팸일까?

이 거래는 이상 거래일까?

내일 상품 수요는 얼마나 될까?

사진 속 동물은 고양이일까, 강아지일까?

이런 문제를 사람이 규칙으로 하나씩 작성하려면 어떻게 될까요?

if monthly_fee >= 100000:
    churn = True
elif support_calls >= 5:
    churn = True
elif contract == "월간":
    churn = True
else:
    churn = False

처음에는 그럴듯해 보입니다.

하지만 현실의 데이터는 이렇게 얌전하지 않습니다.

요금은 비싸지만 5년째 사용하는 고객

문의는 많지만 절대 해지하지 않는 고객

장기 계약인데 갑자기 떠나는 고객

사용량은 적지만 만족도가 높은 고객

규칙을 추가할수록 `if` 문은 덩굴처럼 자라납니다.

조건문 10개
→ 아직 관리 가능

조건문 100개
→ 회의 필요

조건문 1,000개
→ 담당자 휴가 신청

머신러닝은 사람이 모든 규칙을 직접 입력하는 대신, 데이터에서 규칙을 찾아내도록 컴퓨터를 학습시키는 방법입니다.

그리고 Python에서 머신러닝을 처음 시작할 때 가장 널리 사용되는 라이브러리 중 하나가 바로 scikit-learn입니다.

이번 시간에는 데이터로 미래를 점치는 수정구슬이 아니라, 과거 사례를 조사해 합리적인 예측을 내놓는 데이터 탐정단을 만들어 보겠습니다. 🔍🤖

1. scikit-learn이란?

scikit-learn은 Python에서 머신러닝 모델을 만들고 평가하는 데 사용하는 오픈소스 라이브러리입니다.

지도학습과 비지도학습을 지원하며 데이터 전처리, 모델 학습, 모델 선택, 성능 평가, 파이프라인 구성 등 머신러닝 작업에 필요한 다양한 도구를 제공합니다. NumPy, SciPy, Matplotlib 생태계를 기반으로 설계되어 있습니다. 2026년 7월 기준 공식 안정 문서는 scikit-learn 1.9.0을 안내하고 있습니다.

scikit-learn은 다음과 같은 작업에 사용할 수 있습니다.

숫자 예측
범주 분류
고객 그룹 나누기
이상 데이터 탐지
데이터 차원 축소
특성 선택
데이터 전처리
모델 성능 평가
최적 설정 탐색

대표적인 활용 사례는 다음과 같습니다.

주택 가격 예측

고객 이탈 예측

스팸 메일 분류

대출 상환 가능성 예측

상품 수요 예측

불량 제품 판별

고객 성향 군집화

이상 거래 탐지

scikit-learn은 대규모 딥러닝 모델보다 표 형태의 데이터와 전통적인 머신러닝 문제를 다룰 때 특히 강력합니다.

나이
월 이용요금
계약 기간
구매 횟수
접속 시간
상품 종류
지역

이런 데이터가 행과 열로 정리되어 있다면 scikit-learn 탐정단이 출동하기 좋은 사건 현장입니다.

2. 머신러닝이란?

머신러닝은 데이터에서 패턴을 학습해 새로운 데이터의 결과를 예측하거나 구조를 발견하는 기술입니다.

예를 들어 과거 주택 거래 데이터가 있다고 가정해 보겠습니다.

면적방 개수건축 연도역과 거리가격
---:---:----:----:-:
59㎡215년800m4억
84㎡35년300m7억
102㎡42년500m9억

머신러닝 모델은 다음 관계를 학습하려고 합니다.

면적이 커지면 가격이 어떻게 변하는가?

방 개수가 가격에 어떤 영향을 주는가?

건물이 오래될수록 가격은 어떻게 달라지는가?

지하철과 가까울수록 가격이 높아지는가?

학습이 끝난 뒤 새로운 주택 정보가 들어오면 가격을 예측합니다.

면적: 75㎡
방 개수: 3개
건축 연도: 8년
역과 거리: 450m

예상 가격: ?

단, 머신러닝은 진짜 미래를 보는 예언 기술이 아닙니다.

과거 데이터에서 발견한 패턴이 새로운 데이터에서도 어느 정도 유지될 것이라는 가정 아래 예측합니다.

머신러닝 모델:
“수정구슬은 없습니다.
대신 과거 사건 기록은 많이 읽었습니다.”

3. 전통적인 프로그램과 머신러닝의 차이

전통적인 프로그램

전통적인 프로그램은 사람이 규칙을 작성합니다.

데이터 + 사람이 만든 규칙
→ 결과

예:

def classify_temperature(
    temperature: float
) -> str:
    if temperature >= 30:
        return "더움"

    if temperature >= 20:
        return "따뜻함"

    if temperature >= 10:
        return "쌀쌀함"

    return "추움"

사람이 기준을 직접 정했습니다.

머신러닝

머신러닝에서는 데이터와 정답을 모델에 제공합니다.

데이터 + 정답
→ 학습 알고리즘
→ 규칙을 담은 모델

학습된 모델에 새로운 데이터를 넣으면 결과를 예측합니다.

새로운 데이터 + 학습된 모델
→ 예측 결과

비교

구분전통적인 프로그램머신러닝
규칙 작성사람이 직접 작성데이터에서 학습
적합한 문제규칙이 명확한 문제규칙이 복잡한 문제
입력데이터와 규칙데이터
결과 생성작성한 조건에 따라 처리학습한 패턴으로 예측
변경 방법코드를 수정데이터를 추가하거나 재학습

세금 계산처럼 규칙이 명확한 문제에 머신러닝을 사용할 필요는 없습니다.

반대로 수천 개 변수의 복잡한 관계를 사람이 조건문으로 만들기 어려울 때 머신러닝이 빛을 발합니다.

망치가 좋다고 냉장고 문까지 못으로 고정하면 안 됩니다. 🔨

4. 머신러닝의 주요 종류

머신러닝은 크게 다음과 같이 구분할 수 있습니다.

지도학습
비지도학습
준지도학습
강화학습

scikit-learn에서는 지도학습과 비지도학습을 중심으로 다양한 모델을 제공합니다.

지도학습

입력 데이터와 정답을 함께 사용합니다.

입력:
고객 정보

정답:
해지 또는 유지

모델은 입력과 정답의 관계를 학습합니다.

비지도학습

정답 없이 데이터만 사용합니다.

입력:
고객 구매 기록

정답:
없음

모델은 비슷한 고객끼리 묶거나 데이터 안의 구조를 찾습니다.

준지도학습

일부 데이터에만 정답이 있을 때 사용합니다.

정답이 있는 데이터 1,000개
정답이 없는 데이터 100,000개

강화학습

행동에 대한 보상을 받으며 전략을 학습합니다.

게임 플레이, 로봇 제어 등에 활용됩니다.

scikit-learn은 일반적인 강화학습 프레임워크를 목적으로 설계된 라이브러리는 아닙니다.

5. 지도학습과 비지도학습

지도학습

선생님이 문제와 정답을 함께 제공하는 방식입니다.

문제:
이 메일의 내용

정답:
스팸
문제:
주택의 면적과 위치

정답:
실제 거래 가격

모델은 여러 문제와 정답을 살펴본 뒤 새로운 문제를 풉니다.

비지도학습

정답지가 없는 상태에서 데이터의 구조를 찾습니다.

고객 구매 기록을 보고
비슷한 소비 유형끼리 그룹화
뉴스 기사를 보고
비슷한 주제끼리 묶기
정상 데이터와 크게 다른
이상 패턴 찾기

비지도학습 모델은 정답을 맞히는 학생이라기보다, 책상에 흩어진 자료를 특징별로 정리하는 도서관 사서에 가깝습니다. 📚

6. 회귀와 분류의 차이

지도학습은 다시 회귀와 분류로 구분할 수 있습니다.

회귀

연속적인 숫자를 예측합니다.

주택 가격
상품 판매량
내일 기온
배송 시간
전력 사용량

예측 결과:

53,200,000원
28.7℃
1,250개
43분

분류

미리 정의된 범주를 예측합니다.

스팸 / 정상

해지 / 유지

불량 / 정상

고양이 / 강아지

낮음 / 보통 / 높음

한눈에 비교

구분회귀분류
예측 대상연속적인 숫자범주
예시가격, 온도, 수량스팸 여부, 품종
대표 모델LinearRegressionLogisticRegression
대표 평가MAE, RMSE, R²정확도, 정밀도, 재현율

판단 방법은 간단합니다.

예측 결과가 숫자 자체인가?
→ 회귀

예측 결과가 종류나 상태인가?
→ 분류

7. scikit-learn 설치하기

터미널이나 명령 프롬프트에서 다음 명령어를 실행합니다.

python -m pip install scikit-learn

공식 설치 문서는 64비트 Python과 가상환경 사용을 권장하며 가상환경 안에서 scikit-learn을 설치하는 방법을 안내합니다.

실습 라이브러리 함께 설치하기

python -m pip install numpy pandas matplotlib scikit-learn joblib

Windows 가상환경

python -m venv venv
venv\Scripts\activate
python -m pip install numpy pandas matplotlib scikit-learn joblib

macOS 또는 Linux

python3 -m venv venv
source venv/bin/activate
python -m pip install numpy pandas matplotlib scikit-learn joblib

설치 확인

import sklearn

print(sklearn.__version__)

버전이 정상적으로 출력되면 설치가 완료된 것입니다.

라이브러리 이름 주의하기

설치할 때는 다음 이름을 사용합니다.

pip install scikit-learn

Python 코드에서는 `sklearn`이라는 이름으로 불러옵니다.

import sklearn
설치 이름:
scikit-learn

Python 패키지 이름:
sklearn

이름이 다른 이유로 초보자의 첫 번째 추리 사건이 시작됩니다.

8. scikit-learn의 일관된 사용 구조

scikit-learn의 큰 장점은 여러 모델이 비슷한 사용 구조를 가진다는 점입니다.

1단계: 모델 생성

model = SomeModel()

2단계: 학습

model.fit(
    X_train,
    y_train
)

3단계: 예측

predictions = model.predict(
    X_test
)

4단계: 평가

score = model.score(
    X_test,
    y_test
)

전처리 객체

전처리 객체는 주로 다음 구조를 사용합니다.

transformer.fit(
    X_train
)

X_train_transformed = transformer.transform(
    X_train
)

두 작업을 한 번에 수행할 수도 있습니다.

X_train_transformed = (
    transformer.fit_transform(
        X_train
    )
)

핵심 메서드

메서드역할
`fit()`데이터에서 규칙이나 통계 학습
`predict()`새로운 데이터 결과 예측
`transform()`데이터를 다른 형태로 변환
`fit_transform()`학습 후 즉시 변환
`score()`모델의 기본 평가 점수 계산
`predict_proba()`분류 확률 반환
`get_params()`설정값 확인
`set_params()`설정값 변경

scikit-learn은 다양한 모델과 전처리 도구에 일관된 인터페이스를 적용해 모델을 교체하거나 여러 작업을 연결하기 쉽게 설계되어 있습니다.

9. X와 y 이해하기

머신러닝 예제에서는 `X`와 `y`라는 이름이 자주 등장합니다.

X

예측에 사용하는 입력 데이터입니다.

다음과 같은 이름으로도 부릅니다.

특성
feature
독립변수
설명변수
입력변수

예:

면적방 개수건물 나이
59215
8435
10242
X = df[
    [
        "면적",
        "방개수",
        "건물나이"
    ]
]

y

예측하려는 정답입니다.

다음 이름으로도 부릅니다.

타깃
target
종속변수
레이블
정답
y = df["가격"]

배열 형태

일반적으로 `X`는 2차원 구조입니다.

행:
데이터 샘플

열:
특성
X.shape
→ (샘플 개수, 특성 개수)

`y`는 보통 1차원입니다.

y.shape
→ (샘플 개수,)

NumPy 배열, 숫자 리스트, pandas DataFrame과 Series 등 여러 array-like 구조를 scikit-learn 입력으로 사용할 수 있습니다.

10. 첫 번째 머신러닝 모델 만들기

공부 시간으로 시험 점수를 예측하는 간단한 모델을 만들어 보겠습니다.

데이터 준비

import numpy as np

X = np.array([
    [1],
    [2],
    [3],
    [4],
    [5],
    [6]
])

y = np.array([
    52,
    58,
    65,
    73,
    79,
    88
])

`X`가 2차원인 점에 주목하세요.

잘못된 형태:
[1, 2, 3, 4]

올바른 형태:
[[1], [2], [3], [4]]

모델 생성과 학습

from sklearn.linear_model import (
    LinearRegression
)

model = LinearRegression()

model.fit(
    X,
    y
)

새로운 값 예측

new_data = np.array([
    [7]
])

prediction = model.predict(
    new_data
)

print(
    f"7시간 공부 예상 점수: "
    f"{prediction[0]:.2f}"
)

모델이 학습한 값 확인

print(
    f"기울기: {model.coef_[0]:.4f}"
)

print(
    f"절편: {model.intercept_:.4f}"
)

선형회귀는 입력 특성의 선형 결합으로 예측값을 만들고 실제값과 예측값 사이의 잔차 제곱합이 작아지도록 계수를 추정합니다.

모델은 데이터를 보고 대략 다음과 같은 규칙을 학습합니다.

예상 점수
=
공부 시간 × 기울기
+ 절편

첫 번째 머신러닝 모델이 완성되었습니다.

컴퓨터가 갑자기 공부의 철학을 깨달은 것은 아닙니다.

주어진 숫자 사이에서 가장 잘 맞는 직선을 찾은 것입니다. 📏

11. 학습 데이터와 평가 데이터 분리하기

모든 데이터를 학습에 사용하면 모델의 실력을 제대로 평가하기 어렵습니다.

이미 답을 본 문제로 시험을 치르는 것과 같기 때문입니다.

전체 데이터
├─ 학습 데이터
└─ 평가 데이터

학습 데이터

모델이 규칙을 배우는 데 사용합니다.

X_train
y_train

평가 데이터

학습에 사용하지 않은 데이터로 모델을 평가합니다.

X_test
y_test

train_test_split

from sklearn.model_selection import (
    train_test_split
)

X_train, X_test, y_train, y_test = (
    train_test_split(
        X,
        y,
        test_size=0.2,
        random_state=2026
    )
)

`train_test_split()`은 배열이나 행렬을 무작위 학습·평가 부분집합으로 나누는 편의 함수입니다. `test_size`, `random_state`, `shuffle`, `stratify` 등을 설정할 수 있습니다.

test_size

test_size=0.2

전체 데이터의 20%를 평가용으로 사용합니다.

학습 데이터:
80%

평가 데이터:
20%

분류 문제의 stratify

분류 데이터의 클래스 비율을 비슷하게 유지하려면 다음과 같이 작성합니다.

X_train, X_test, y_train, y_test = (
    train_test_split(
        X,
        y,
        test_size=0.2,
        random_state=2026,
        stratify=y
    )
)

예를 들어 전체 데이터가 다음 비율이라면,

유지 고객:
80%

해지 고객:
20%

학습 데이터와 평가 데이터도 비슷한 비율로 나누도록 돕습니다.

12. random_state는 무엇인가?

데이터를 무작위로 나누면 실행할 때마다 결과가 달라질 수 있습니다.

train_test_split(
    X,
    y,
    test_size=0.2
)

오늘 실행한 평가 데이터와 내일 실행한 평가 데이터가 달라질 수 있습니다.

`random_state`에 정수를 지정하면 같은 무작위 결과를 재현할 수 있습니다.

train_test_split(
    X,
    y,
    test_size=0.2,
    random_state=2026
)

왜 필요한가?

실험 재현

동료와 결과 비교

오류 원인 분석

예제 결과 고정

모델 변경 전후 비교

`random_state=2026`에서 숫자 2026 자체에 특별한 머신러닝 능력은 없습니다.

같은 정수를 사용하면 같은 무작위 순서를 재현한다는 점이 중요합니다.

random_state:
무작위 세계에 꽂아 두는 책갈피

13. 회귀 모델 만들기

주택 정보를 이용해 가격을 예측해 보겠습니다.

데이터 준비

import pandas as pd

data = pd.DataFrame({
    "면적": [
        45, 52, 59, 66, 72,
        79, 84, 91, 102, 115,
        125, 138
    ],
    "방개수": [
        1, 2, 2, 2, 3,
        3, 3, 3, 4, 4,
        4, 5
    ],
    "건물나이": [
        25, 22, 18, 15, 12,
        10, 8, 7, 5, 4,
        3, 2
    ],
    "역거리": [
        1200, 1000, 900, 850, 700,
        650, 500, 450, 400, 350,
        300, 250
    ],
    "가격": [
        28000, 33000, 37000, 41000,
        47000, 52000, 58000, 62000,
        71000, 79000, 86000, 97000
    ]
})

가격 단위는 만 원이라고 가정합니다.

X와 y 분리

X = data[
    [
        "면적",
        "방개수",
        "건물나이",
        "역거리"
    ]
]

y = data["가격"]

학습·평가 데이터 분리

from sklearn.model_selection import (
    train_test_split
)

X_train, X_test, y_train, y_test = (
    train_test_split(
        X,
        y,
        test_size=0.25,
        random_state=2026
    )
)

모델 학습

from sklearn.linear_model import (
    LinearRegression
)

model = LinearRegression()

model.fit(
    X_train,
    y_train
)

예측

predictions = model.predict(
    X_test
)

result = pd.DataFrame({
    "실제가격": y_test,
    "예측가격": predictions
})

print(result)

새로운 주택 예측

new_house = pd.DataFrame({
    "면적": [88],
    "방개수": [3],
    "건물나이": [6],
    "역거리": [420]
})

predicted_price = model.predict(
    new_house
)

print(
    f"예상 가격: "
    f"{predicted_price[0]:,.0f}만 원"
)

14. 회귀 모델 평가하기

회귀 모델은 실제값과 예측값의 차이를 이용해 평가합니다.

MAE

MAE는 평균 절대 오차입니다.

실제값과 예측값 차이의
절댓값 평균
from sklearn.metrics import (
    mean_absolute_error
)

mae = mean_absolute_error(
    y_test,
    predictions
)

print(
    f"MAE: {mae:.2f}"
)

MAE는 0에 가까울수록 좋습니다.

RMSE

RMSE는 오차를 제곱해 평균한 뒤 제곱근을 계산합니다.

from sklearn.metrics import (
    root_mean_squared_error
)

rmse = root_mean_squared_error(
    y_test,
    predictions
)

print(
    f"RMSE: {rmse:.2f}"
)

RMSE도 0에 가까울수록 좋으며 큰 오차에 상대적으로 더 큰 영향을 받습니다. scikit-learn은 `root_mean_squared_error()` 함수를 제공합니다.

R² 점수

from sklearn.metrics import r2_score

r2 = r2_score(
    y_test,
    predictions
)

print(
    f"R²: {r2:.4f}"
)

R²은 모델이 타깃 값의 변동을 어느 정도 설명하는지 나타내는 지표입니다.

일반적으로 1에 가까울수록 좋지만 음수가 나올 수도 있습니다.

한 번에 출력하기

print(
    f"MAE: {mae:,.2f}"
)

print(
    f"RMSE: {rmse:,.2f}"
)

print(
    f"R²: {r2:.4f}"
)

평가 지표 비교

지표의미좋은 방향
MAE절대 오차 평균작을수록 좋음
RMSE제곱 오차 기반작을수록 좋음
변동 설명 정도일반적으로 클수록 좋음

평가 지표는 모델의 성적표입니다.

단, 국어 점수 하나만 보고 학생의 모든 능력을 판단할 수 없듯 지표 하나만으로 모델을 판단해서는 안 됩니다.

15. 분류 모델 만들기

이번에는 붓꽃의 측정값으로 품종을 예측해 보겠습니다.

scikit-learn에는 학습용으로 사용할 수 있는 여러 내장 데이터셋이 있습니다.

데이터 불러오기

from sklearn.datasets import load_iris

iris = load_iris(
    as_frame=True
)

print(iris.data.head())
print(iris.target.head())

데이터 구조

X = iris.data
y = iris.target

print(X.shape)
print(y.shape)
print(iris.target_names)

붓꽃 데이터에는 꽃받침과 꽃잎의 길이·너비가 들어 있습니다.

sepal length
sepal width
petal length
petal width

분류할 품종은 세 가지입니다.

setosa
versicolor
virginica

데이터 분리

from sklearn.model_selection import (
    train_test_split
)

X_train, X_test, y_train, y_test = (
    train_test_split(
        X,
        y,
        test_size=0.2,
        random_state=2026,
        stratify=y
    )
)

로지스틱 회귀 모델

이름에 회귀가 들어 있지만 `LogisticRegression`은 분류 모델입니다.

from sklearn.linear_model import (
    LogisticRegression
)

model = LogisticRegression(
    max_iter=1000
)

model.fit(
    X_train,
    y_train
)

scikit-learn의 `LogisticRegression`은 기본적으로 규제가 적용되는 로지스틱 회귀 분류기이며 밀집 데이터와 희소 데이터를 모두 처리할 수 있습니다.

예측

predictions = model.predict(
    X_test
)

print(predictions)

실제 품종 이름으로 변환

predicted_names = (
    iris.target_names[predictions]
)

print(predicted_names)

16. 분류 모델 평가하기

정확도

from sklearn.metrics import (
    accuracy_score
)

accuracy = accuracy_score(
    y_test,
    predictions
)

print(
    f"정확도: {accuracy:.4f}"
)

정확도는 전체 예측 중 올바르게 맞힌 비율입니다.

100개 중 92개를 맞힘
→ 정확도 0.92

classification_report

from sklearn.metrics import (
    classification_report
)

print(
    classification_report(
        y_test,
        predictions,
        target_names=iris.target_names,
        zero_division=0
    )
)

`classification_report()`는 클래스별 정밀도, 재현율, F1 점수와 데이터 개수를 요약해 보여 줍니다.

결과 예시:

precision  recall  f1-score  support

setosa            1.00    1.00      1.00       10
versicolor        0.91    1.00      0.95       10
virginica         1.00    0.90      0.95       10

17. 혼동행렬 이해하기

혼동행렬은 모델이 어떤 정답을 어떤 값으로 예측했는지 표로 보여 줍니다.

계산하기

from sklearn.metrics import (
    confusion_matrix
)

matrix = confusion_matrix(
    y_test,
    predictions
)

print(matrix)

혼동행렬의 행은 실제 클래스, 열은 예측 클래스를 나타냅니다.

시각화하기

import matplotlib.pyplot as plt

from sklearn.metrics import (
    ConfusionMatrixDisplay
)

ConfusionMatrixDisplay.from_predictions(
    y_test,
    predictions,
    display_labels=iris.target_names,
    cmap="Blues"
)

plt.title(
    "Iris Confusion Matrix"
)

plt.tight_layout()
plt.show()

이진 분류 혼동행렬

고객 이탈 여부처럼 클래스가 두 개라면 다음 네 가지 결과가 나옵니다.

실제예측이름
유지유지TN
유지해지FP
해지유지FN
해지해지TP
TP:
실제 해지 고객을 해지로 예측

TN:
실제 유지 고객을 유지로 예측

FP:
유지 고객을 해지로 잘못 예측

FN:
해지 고객을 유지로 잘못 예측

모델이 틀렸다는 사실만 확인하는 것이 아니라 어떤 방향으로 틀렸는지 확인할 수 있습니다.

18. 정확도만 믿으면 위험한 이유

1,000건의 거래 중 이상 거래가 10건이라고 가정해 보겠습니다.

정상 거래:
990건

이상 거래:
10건

모델이 모든 거래를 정상이라고 예측한다면 어떻게 될까요?

맞힌 정상 거래:
990건

놓친 이상 거래:
10건

정확도:

990 / 1000
= 99%

정확도는 99%이지만 이상 거래를 단 한 건도 찾지 못했습니다.

모델:
“정확도 99%입니다.”

보안 담당자:
“이상 거래는 몇 건 찾았나요?”

모델:
“그 질문은 다음 회의에서…”

클래스 비율이 크게 불균형한 문제에서는 정확도만으로 성능을 평가하면 중요한 오류를 놓칠 수 있습니다.

다음 지표를 함께 확인해야 합니다.

정밀도
재현율
F1 점수
ROC AUC
PR AUC
혼동행렬

19. 정밀도와 재현율

정밀도

모델이 양성이라고 예측한 것 중 실제 양성의 비율입니다.

정밀도
=
TP / (TP + FP)

예:

이상 거래라고 경고한 10건 중
실제 이상 거래가 8건

정밀도:
8 / 10 = 0.8

정밀도가 중요할 수 있는 사례:

스팸 메일 차단

정상 고객 제재

자동 콘텐츠 삭제

거짓 경고의 비용이 클 때 중요합니다.

재현율

실제 양성 중 모델이 찾아낸 비율입니다.

재현율
=
TP / (TP + FN)

scikit-learn 공식 문서는 재현율을 실제 양성 샘플을 찾아내는 능력으로 설명합니다.

예:

실제 이상 거래 20건 중
15건 탐지

재현율:
15 / 20 = 0.75

재현율이 중요할 수 있는 사례:

위험 거래 탐지

화재 감지

장비 고장 사전 탐지

놓치는 비용이 클 때 중요합니다.

코드

from sklearn.metrics import (
    precision_score,
    recall_score
)

precision = precision_score(
    y_test,
    predictions
)

recall = recall_score(
    y_test,
    predictions
)

20. F1 점수와 ROC AUC

F1 점수

F1 점수는 정밀도와 재현율의 조화평균입니다.

정밀도만 높고 재현율이 매우 낮은 모델

또는

재현율만 높고 정밀도가 매우 낮은 모델

두 상황을 균형 있게 평가하는 데 활용할 수 있습니다.

from sklearn.metrics import f1_score

f1 = f1_score(
    y_test,
    predictions
)

print(
    f"F1 점수: {f1:.4f}"
)

다중 클래스에서는 평균 방식을 지정합니다.

f1 = f1_score(
    y_test,
    predictions,
    average="macro"
)

예측 확률

분류 모델은 클래스뿐 아니라 예측 확률을 제공하기도 합니다.

probabilities = model.predict_proba(
    X_test
)

print(probabilities[:5])

이진 분류에서 양성 클래스 확률:

positive_probabilities = (
    model.predict_proba(
        X_test
    )[:, 1]
)

ROC AUC

from sklearn.metrics import (
    roc_auc_score
)

roc_auc = roc_auc_score(
    y_test,
    positive_probabilities
)

print(
    f"ROC AUC: {roc_auc:.4f}"
)

ROC AUC는 여러 분류 임계값에서 모델의 구분 능력을 평가하며 이진, 다중 클래스, 다중 레이블 문제에 제한 조건에 따라 사용할 수 있습니다.

ROC 곡선

import matplotlib.pyplot as plt

from sklearn.metrics import (
    RocCurveDisplay
)

RocCurveDisplay.from_estimator(
    model,
    X_test,
    y_test
)

plt.title(
    "ROC Curve"
)

plt.show()

21. 데이터 전처리가 필요한 이유

현실의 데이터는 머신러닝 모델에게 바로 전달하기 어려운 경우가 많습니다.

빈 값이 존재함

문자열 범주가 포함됨

숫자 단위가 크게 다름

이상치가 존재함

날짜가 문자열로 저장됨

불필요한 열이 포함됨

예:

나이월요금계약가입기간
3259000월간3
결측1100001년24
48결측2년53

머신러닝 모델에 전달하기 전에 다음 작업이 필요할 수 있습니다.

결측값 처리

문자열 숫자 변환

범주형 데이터 인코딩

특성 스케일 조정

이상치 처리

불필요한 특성 제거

데이터 전처리는 모델에게 사건 파일을 읽기 좋은 순서로 정리해 주는 작업입니다.

서류가 젖어 있고 이름이 지워진 상태라면 명탐정도 곤란합니다.

22. 특성 스케일링

다음 두 특성을 비교해 보겠습니다.

나이:
18 ~ 80

연봉:
20,000,000 ~ 500,000,000

숫자 크기가 크게 다릅니다.

거리나 기울기를 이용하는 일부 알고리즘에서는 큰 범위의 특성이 계산에 과도한 영향을 줄 수 있습니다.

StandardScaler

from sklearn.preprocessing import (
    StandardScaler
)

scaler = StandardScaler()

X_train_scaled = scaler.fit_transform(
    X_train
)

X_test_scaled = scaler.transform(
    X_test
)

`StandardScaler`는 학습 데이터의 평균을 빼고 표준편차로 나누어 특성을 표준화합니다.

변환 후 평균:
대략 0

변환 후 표준편차:
대략 1

중요한 규칙

학습 데이터에는 `fit_transform()`을 사용합니다.

X_train_scaled = (
    scaler.fit_transform(
        X_train
    )
)

평가 데이터에는 `transform()`만 사용합니다.

X_test_scaled = scaler.transform(
    X_test
)

다음처럼 평가 데이터에 다시 `fit_transform()`을 사용하면 안 됩니다.

# 잘못된 예
X_test_scaled = (
    scaler.fit_transform(
        X_test
    )
)

학습 데이터와 평가 데이터가 서로 다른 평균과 표준편차로 변환되기 때문입니다.

MinMaxScaler

값을 지정한 범위로 변환할 수도 있습니다.

from sklearn.preprocessing import (
    MinMaxScaler
)

scaler = MinMaxScaler(
    feature_range=(0, 1)
)

`MinMaxScaler`는 학습 데이터의 각 특성을 기본적으로 0에서 1 범위로 변환합니다.

23. 범주형 데이터 인코딩

머신러닝 모델은 다음 문자열을 그대로 계산하지 못하는 경우가 많습니다.

계약:
월간
1년
2년

잘못된 숫자 부여

월간 → 0
1년 → 1
2년 → 2

이렇게 변환하면 모델이 다음 관계를 오해할 수 있습니다.

2년 계약은
1년 계약보다 정확히 두 배다?

순서나 간격이 없는 범주에는 One-Hot Encoding을 사용할 수 있습니다.

OneHotEncoder

from sklearn.preprocessing import (
    OneHotEncoder
)

encoder = OneHotEncoder(
    handle_unknown="ignore"
)

변환 결과:

계약_월간
계약_1년
계약_2년

월간 계약:

[1, 0, 0]

1년 계약:

[0, 1, 0]

2년 계약:

[0, 0, 1]

`OneHotEncoder`는 범주마다 이진 열을 생성하며 `sparse_output` 설정에 따라 희소배열 또는 밀집배열을 반환할 수 있습니다.

새로운 범주 처리

encoder = OneHotEncoder(
    handle_unknown="ignore"
)

학습할 때 없었던 범주가 평가나 운영 데이터에 등장해도 오류 대신 안전하게 처리하도록 설정합니다.

LabelEncoder 주의

`LabelEncoder`는 입력 특성 `X`보다 타깃 레이블 `y`를 인코딩하는 용도로 설계되었습니다.

from sklearn.preprocessing import (
    LabelEncoder
)

encoder = LabelEncoder()

encoded_y = encoder.fit_transform(
    y
)

24. 결측값 처리

데이터에 빈 값이 있으면 많은 모델이 오류를 발생시킵니다.

import numpy as np

values = np.array([
    [20, 50000],
    [30, np.nan],
    [np.nan, 70000]
])

SimpleImputer

from sklearn.impute import SimpleImputer

imputer = SimpleImputer(
    strategy="median"
)

transformed = imputer.fit_transform(
    values
)

print(transformed)

대표 전략

mean
→ 평균으로 대체

median
→ 중앙값으로 대체

most_frequent
→ 가장 자주 등장한 값으로 대체

constant
→ 지정한 값으로 대체

숫자 데이터:

numeric_imputer = SimpleImputer(
    strategy="median"
)

범주형 데이터:

categorical_imputer = SimpleImputer(
    strategy="most_frequent"
)

결측값을 무조건 0으로 채우면 안 됩니다.

월 이용요금 결측
→ 0원이라고 단정?

나이 결측
→ 0살이라고 단정?

계약 종류 결측
→ 월간이라고 단정?

결측값이 왜 발생했는지 먼저 확인해야 합니다.

빈칸도 데이터가 남긴 메시지일 수 있습니다.

25. ColumnTransformer 활용하기

실무 데이터에는 숫자 열과 범주형 열이 함께 존재합니다.

숫자 열:
나이
월 이용요금
가입 기간
문의 횟수

범주형 열:
계약 종류
결제 방법
지역

숫자 열에는 스케일링을 적용하고 범주형 열에는 원-핫 인코딩을 적용해야 합니다.

`ColumnTransformer`를 사용하면 열 종류별로 서로 다른 전처리를 적용할 수 있습니다. 변환 결과는 하나의 특성 공간으로 결합됩니다.

전처리 파이프라인

from sklearn.compose import (
    ColumnTransformer
)
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import (
    OneHotEncoder,
    StandardScaler
)


numeric_columns = [
    "나이",
    "월요금",
    "가입기간",
    "문의횟수"
]

categorical_columns = [
    "계약",
    "결제방법"
]


numeric_pipeline = Pipeline([
    (
        "imputer",
        SimpleImputer(
            strategy="median"
        )
    ),
    (
        "scaler",
        StandardScaler()
    )
])


categorical_pipeline = Pipeline([
    (
        "imputer",
        SimpleImputer(
            strategy="most_frequent"
        )
    ),
    (
        "encoder",
        OneHotEncoder(
            handle_unknown="ignore"
        )
    )
])


preprocessor = ColumnTransformer([
    (
        "numeric",
        numeric_pipeline,
        numeric_columns
    ),
    (
        "categorical",
        categorical_pipeline,
        categorical_columns
    )
])

이제 전처리 객체 하나가 다음 작업을 담당합니다.

숫자 결측값 처리
→ 숫자 스케일링
→ 범주 결측값 처리
→ 범주 원-핫 인코딩
→ 결과 결합

데이터 전처리 부서가 만들어졌습니다.

26. Pipeline으로 전처리와 모델 연결하기

전처리와 모델을 따로 관리하면 실수가 발생하기 쉽습니다.

X_train_processed = (
    preprocessor.fit_transform(
        X_train
    )
)

model.fit(
    X_train_processed,
    y_train
)

예측할 때도 같은 전처리를 적용해야 합니다.

X_test_processed = (
    preprocessor.transform(
        X_test
    )
)

predictions = model.predict(
    X_test_processed
)

Pipeline을 사용하면 이 과정을 하나로 묶을 수 있습니다.

from sklearn.linear_model import (
    LogisticRegression
)
from sklearn.pipeline import Pipeline


pipeline = Pipeline([
    (
        "preprocessor",
        preprocessor
    ),
    (
        "model",
        LogisticRegression(
            max_iter=1000
        )
    )
])

학습

pipeline.fit(
    X_train,
    y_train
)

내부 동작:

X_train
→ 결측값 처리
→ 스케일링
→ 원-핫 인코딩
→ 모델 학습

예측

predictions = pipeline.predict(
    X_test
)

내부 동작:

X_test
→ 학습된 규칙으로 전처리
→ 모델 예측

Pipeline은 여러 전처리 객체를 순서대로 적용하고 마지막에 예측 모델을 연결할 수 있습니다. 교차검증과 하이퍼파라미터 탐색에서도 전체 과정을 함께 평가할 수 있습니다.

Pipeline은 공장의 컨베이어 벨트와 같습니다.

원재료가 들어오면 세척, 포장, 검사, 출고가 같은 순서로 진행됩니다. 🏭

27. 데이터 누수란?

데이터 누수는 실제 예측 시점에는 알 수 없는 정보가 모델 학습에 사용되는 문제입니다.

데이터 누수가 발생하면 평가 성능은 매우 좋아 보이지만 실제 운영에서는 성능이 크게 떨어질 수 있습니다. 공식 문서는 테스트 데이터에 `fit()`을 수행하지 않는 것을 기본 원칙으로 설명합니다.

잘못된 전처리

# 잘못된 예
X_scaled = scaler.fit_transform(
    X
)

X_train, X_test, y_train, y_test = (
    train_test_split(
        X_scaled,
        y
    )
)

전체 데이터의 평균과 표준편차를 먼저 계산했기 때문에 평가 데이터 정보가 전처리에 포함되었습니다.

올바른 순서

X_train, X_test, y_train, y_test = (
    train_test_split(
        X,
        y,
        test_size=0.2,
        random_state=2026
    )
)

scaler.fit(
    X_train
)

X_train_scaled = scaler.transform(
    X_train
)

X_test_scaled = scaler.transform(
    X_test
)

Pipeline을 사용하면 이런 실수를 줄일 수 있습니다.

노골적인 데이터 누수

고객 해지를 예측하면서 다음 열을 입력으로 사용한다고 가정해 보겠습니다.

해지 처리 날짜
해지 사유
해지 완료 여부

예측 시점에는 존재하지 않는 정보입니다.

모델:
“해지 완료 여부가 1이면
해지할 확률이 높습니다.”

개발자:
“그건 이미 해지한 뒤에 생기는 값인데요?”

시간 데이터 누수

과거 데이터로 미래를 예측하면서 미래 데이터를 학습에 포함하면 안 됩니다.

시간 순서가 있는 데이터에는 일반적인 무작위 분할보다 `TimeSeriesSplit` 같은 방법을 검토해야 합니다. 공식 문서는 미래 데이터로 학습하고 과거를 평가하는 문제를 피하기 위해 시간 순서 데이터에 별도의 분할 전략을 제공합니다.

28. 과소적합과 과적합

과소적합

모델이 너무 단순해 데이터의 패턴을 충분히 학습하지 못한 상태입니다.

학습 성능:
낮음

평가 성능:
낮음

예:

복잡한 곡선 관계를
직선 하나로만 설명

과적합

모델이 학습 데이터를 지나치게 세밀하게 기억한 상태입니다.

학습 성능:
매우 높음

평가 성능:
낮음

예:

시험 문제와 답을 통째로 암기

숫자만 바뀐 새 문제는 틀림

적절한 모델

학습 데이터의 주요 패턴을 학습하면서

새로운 데이터에도
잘 적용되는 모델

비교

상태학습 성능평가 성능
과소적합낮음낮음
적절한 학습높음높음
과적합매우 높음낮음

과적합을 줄이는 방법

더 많은 데이터 확보

모델 복잡도 낮추기

규제 적용

불필요한 특성 제거

교차검증 사용

트리 깊이 제한

조기 종료

데이터 누수 제거

모델도 공부를 잘못하면 두 종류로 나뉩니다.

과소적합:
교과서를 거의 안 읽음

과적합:
작년 시험 답안만 외움

29. 교차검증

한 번의 학습·평가 분할 결과는 우연히 좋거나 나쁠 수 있습니다.

교차검증은 데이터를 여러 부분으로 나누어 반복적으로 학습하고 평가합니다.

5겹 교차검증

Fold 1:
1번 묶음 평가, 나머지 학습

Fold 2:
2번 묶음 평가, 나머지 학습

Fold 3:
3번 묶음 평가, 나머지 학습

Fold 4:
4번 묶음 평가, 나머지 학습

Fold 5:
5번 묶음 평가, 나머지 학습

각 묶음이 한 번씩 평가 데이터가 됩니다.

cross_val_score

from sklearn.model_selection import (
    cross_val_score
)

scores = cross_val_score(
    pipeline,
    X,
    y,
    cv=5,
    scoring="accuracy"
)

print(scores)

print(
    f"평균 점수: "
    f"{scores.mean():.4f}"
)

print(
    f"표준편차: "
    f"{scores.std():.4f}"
)

`cross_val_score()`는 각 교차검증 실행에서 얻은 평가 점수 배열을 반환합니다. 여러 지표와 학습 시간까지 확인하려면 `cross_validate()`를 사용할 수 있습니다.

StratifiedKFold

분류 문제에서는 클래스 비율을 유지하는 분할을 사용할 수 있습니다.

from sklearn.model_selection import (
    StratifiedKFold
)

cv = StratifiedKFold(
    n_splits=5,
    shuffle=True,
    random_state=2026
)

scores = cross_val_score(
    pipeline,
    X,
    y,
    cv=cv,
    scoring="f1"
)

교차검증은 한 명의 심사위원에게만 평가받지 않고 여러 심사위원에게 돌아가며 평가받는 방식입니다.

30. 하이퍼파라미터란?

머신러닝에는 두 종류의 값이 있습니다.

모델이 학습하는 값

데이터를 통해 자동으로 결정됩니다.

선형회귀 계수

로지스틱 회귀 계수

결정트리 분할 기준

사람이 지정하는 값

모델 학습 전에 설정합니다.

이를 하이퍼파라미터라고 합니다.

트리 최대 깊이

트리 개수

규제 강도

이웃 개수

학습률

예:

from sklearn.ensemble import (
    RandomForestClassifier
)

model = RandomForestClassifier(
    n_estimators=300,
    max_depth=8,
    min_samples_split=5,
    random_state=2026
)
n_estimators
→ 생성할 트리 개수

max_depth
→ 트리 최대 깊이

min_samples_split
→ 노드를 나누기 위한 최소 샘플 수

하이퍼파라미터는 모델의 성격을 결정하는 조절 손잡이입니다.

손잡이가 많다고 전부 최대로 돌리면 기계가 강력해지는 것은 아닙니다. 🎛️

31. GridSearchCV 활용하기

GridSearchCV는 지정한 하이퍼파라미터 조합을 모두 시도하고 교차검증을 통해 좋은 조합을 찾습니다.

모델 파이프라인

from sklearn.ensemble import (
    RandomForestClassifier
)
from sklearn.pipeline import Pipeline

pipeline = Pipeline([
    (
        "preprocessor",
        preprocessor
    ),
    (
        "model",
        RandomForestClassifier(
            random_state=2026
        )
    )
])

탐색 범위

parameter_grid = {
    "model__n_estimators": [
        100,
        300
    ],
    "model__max_depth": [
        None,
        5,
        10
    ],
    "model__min_samples_split": [
        2,
        5
    ]
}

Pipeline 내부 설정은 다음 형식으로 지정합니다.

단계이름__파라미터이름
model__max_depth

탐색 실행

from sklearn.model_selection import (
    GridSearchCV
)

search = GridSearchCV(
    estimator=pipeline,
    param_grid=parameter_grid,
    scoring="f1",
    cv=5,
    n_jobs=-1,
    verbose=1
)

search.fit(
    X_train,
    y_train
)

결과 확인

print(
    "최적 설정:"
)

print(
    search.best_params_
)

print(
    f"최고 교차검증 점수: "
    f"{search.best_score_:.4f}"
)

최적 모델

best_model = search.best_estimator_

predictions = best_model.predict(
    X_test
)

GridSearchCV는 꼼꼼하지만 조합이 많으면 계산량이 빠르게 증가합니다.

트리 개수 4가지
× 깊이 5가지
× 최소 샘플 4가지
× 교차검증 5회

= 모델 학습 400회

모델 체력장이 열릴 수 있습니다.

32. RandomizedSearchCV 활용하기

RandomizedSearchCV는 모든 조합을 시도하지 않고 지정된 횟수만큼 무작위 조합을 선택합니다.

from scipy.stats import randint

from sklearn.model_selection import (
    RandomizedSearchCV
)


parameter_distributions = {
    "model__n_estimators": randint(
        100,
        600
    ),
    "model__max_depth": [
        None,
        5,
        10,
        15,
        20
    ],
    "model__min_samples_split": randint(
        2,
        15
    ),
    "model__min_samples_leaf": randint(
        1,
        10
    )
}


random_search = RandomizedSearchCV(
    estimator=pipeline,
    param_distributions=(
        parameter_distributions
    ),
    n_iter=30,
    scoring="f1",
    cv=5,
    random_state=2026,
    n_jobs=-1
)

random_search.fit(
    X_train,
    y_train
)

RandomizedSearchCV는 하이퍼파라미터 설정을 교차검증으로 평가하며 지정한 횟수만큼 설정 조합을 추출합니다.

GridSearchCV와 비교

구분GridSearchCVRandomizedSearchCV
탐색 방법모든 조합일부 무작위 조합
장점지정 범위를 꼼꼼히 탐색큰 범위를 빠르게 탐색
단점조합이 많으면 느림최적 조합을 놓칠 수 있음
활용작은 탐색 공간큰 탐색 공간

실무에서는 RandomizedSearchCV로 넓게 탐색한 뒤 GridSearchCV로 좁은 범위를 정밀 탐색하기도 합니다.

33. 결정트리와 랜덤 포레스트

결정트리

결정트리는 질문을 반복하며 데이터를 나눕니다.

가입 기간이 6개월 미만인가?
├─ 예
│  └─ 월간 계약인가?
│     ├─ 예 → 해지 가능성 높음
│     └─ 아니오 → 중간
└─ 아니오
   └─ 문의 횟수가 4회 이상인가?
      ├─ 예 → 해지 가능성 중간
      └─ 아니오 → 유지 가능성 높음

장점:

구조를 이해하기 쉬움

숫자 스케일링이 필수는 아님

비선형 관계 처리 가능

특성 간 상호작용 학습 가능

단점:

깊게 성장하면 과적합 가능

데이터 변화에 민감

단일 트리 성능이 불안정할 수 있음

랜덤 포레스트

랜덤 포레스트는 여러 결정트리를 만들고 결과를 종합합니다.

1번 트리:
해지

2번 트리:
유지

3번 트리:
해지

4번 트리:
해지

최종:
해지

공식 문서에서 랜덤 포레스트는 여러 데이터 부분집합에 다수의 결정트리를 학습하고 예측을 평균하거나 투표해 정확도를 개선하고 과적합을 제어하는 앙상블 모델로 설명됩니다.

코드

from sklearn.ensemble import (
    RandomForestClassifier
)

model = RandomForestClassifier(
    n_estimators=300,
    max_depth=10,
    min_samples_split=5,
    random_state=2026,
    n_jobs=-1
)

model.fit(
    X_train,
    y_train
)

단일 탐정의 추리보다 여러 탐정의 독립적인 의견을 종합하는 방식입니다.

단, 탐정 300명을 부르면 커피값과 계산 시간이 늘어납니다. ☕

34. 비지도학습과 군집화

비지도학습에서는 정답 `y` 없이 입력 데이터 `X`만 사용합니다.

대표적인 군집화 알고리즘으로 K-Means가 있습니다.

고객 데이터

import numpy as np

customers = np.array([
    [20, 2],
    [25, 3],
    [22, 2],
    [80, 15],
    [90, 18],
    [85, 16],
    [45, 7],
    [50, 8],
    [48, 6]
])

열의 의미:

월 구매금액
월 방문횟수

K-Means

from sklearn.cluster import KMeans
from sklearn.preprocessing import (
    StandardScaler
)

scaler = StandardScaler()

customers_scaled = scaler.fit_transform(
    customers
)

model = KMeans(
    n_clusters=3,
    random_state=2026,
    n_init="auto"
)

clusters = model.fit_predict(
    customers_scaled
)

print(clusters)

결과 결합

import pandas as pd

result = pd.DataFrame(
    customers,
    columns=[
        "구매금액",
        "방문횟수"
    ]
)

result["그룹"] = clusters

print(result)

K-Means는 비슷한 위치의 데이터를 지정한 개수의 군집으로 나눕니다.

다만 알고리즘이 생성한 그룹이 자동으로 다음 의미를 갖는 것은 아닙니다.

0번 그룹:
VIP

1번 그룹:
일반

2번 그룹:
이탈 위험

그룹의 특성을 분석해 사람이 의미를 해석해야 합니다.

모델은 고객에게 이름표를 붙이지 않습니다.

비슷한 사람끼리 자리만 안내합니다. 🪑

35. 특성 중요도와 모델 해석

좋은 성능만큼 중요한 질문이 있습니다.

모델은 왜 이런 예측을 했는가?

선형 모델 계수

Pipeline에서 전처리 후 특성 이름을 가져올 수 있습니다.

feature_names = (
    pipeline
    .named_steps["preprocessor"]
    .get_feature_names_out()
)

로지스틱 회귀 계수:

coefficients = (
    pipeline
    .named_steps["model"]
    .coef_[0]
)

DataFrame으로 정리:

import numpy as np
import pandas as pd

importance = pd.DataFrame({
    "특성": feature_names,
    "계수": coefficients,
    "절대값": np.abs(
        coefficients
    )
})

importance = importance.sort_values(
    "절대값",
    ascending=False
)

print(
    importance.head(10)
)

계수 해석 주의

양의 계수
→ 다른 조건이 같을 때 양성 클래스 방향

음의 계수
→ 다른 조건이 같을 때 음성 클래스 방향

하지만 계수가 크다고 반드시 인과관계가 있다는 뜻은 아닙니다.

특성 스케일, 다중공선성, 인코딩 방식에 따라 해석이 달라질 수 있습니다.

랜덤 포레스트 특성 중요도

importances = (
    forest_model
    .feature_importances_
)

트리 기반 중요도도 절대적인 원인 설명은 아닙니다.

성능과 해석 가능성은 서로 다른 질문입니다.

잘 맞히는가?

왜 그렇게 판단했는가?

공정하게 판단했는가?

운영 환경에서도 유지되는가?

이 네 질문을 따로 확인해야 합니다.

36. 머신러닝 전체 작업 순서

머신러닝 프로젝트는 대체로 다음 흐름으로 진행됩니다.

1. 문제 정의

2. 데이터 수집

3. 데이터 탐색

4. 데이터 정제

5. X와 y 정의

6. 학습·평가 데이터 분리

7. 전처리 설계

8. 기준 모델 학습

9. 성능 평가

10. 오류 분석

11. 교차검증

12. 하이퍼파라미터 튜닝

13. 최종 평가

14. 모델 저장

15. 서비스 적용

16. 성능 모니터링

17. 재학습

가장 먼저 해야 할 일

모델을 선택하기 전에 문제를 명확히 정의해야 합니다.

무엇을 예측할 것인가?

예측 시점은 언제인가?

사용 가능한 데이터는 무엇인가?

잘못 예측했을 때 비용은 얼마인가?

어떤 지표를 우선할 것인가?

기준 모델

복잡한 모델부터 시작하지 않습니다.

회귀:
평균값 예측
선형회귀

분류:
가장 많은 클래스 예측
로지스틱 회귀

단순한 모델을 기준점으로 삼고 복잡한 모델이 실제로 개선되는지 확인합니다.

고급 모델이라는 이유만으로 자동으로 좋은 모델이 되지는 않습니다.

스포일러가 화려하다고 자동차가 빨라지는 것은 아닙니다.

37. 실전 미니 프로젝트: 고객 이탈 예측 시스템

이번에는 고객이 서비스를 해지할 가능성을 예측하는 전체 프로젝트를 만들어 보겠습니다.

프로젝트 목표

가상 고객 데이터 생성

숫자·범주형 특성 구성

일부 결측값 삽입

학습·평가 데이터 분리

전처리 Pipeline 구성

로지스틱 회귀 학습

정확도·정밀도·재현율·F1·ROC AUC 평가

혼동행렬과 ROC 곡선 출력

교차검증 수행

특성 계수 확인

모델 파일 저장

전체 코드

from pathlib import Path

import joblib
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

from sklearn.compose import (
    ColumnTransformer
)
from sklearn.impute import SimpleImputer
from sklearn.linear_model import (
    LogisticRegression
)
from sklearn.metrics import (
    ConfusionMatrixDisplay,
    RocCurveDisplay,
    accuracy_score,
    classification_report,
    f1_score,
    precision_score,
    recall_score,
    roc_auc_score
)
from sklearn.model_selection import (
    StratifiedKFold,
    cross_val_score,
    train_test_split
)
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import (
    OneHotEncoder,
    StandardScaler
)


MODEL_FILE = Path(
    "customer_churn_model.joblib"
)

CONFUSION_FILE = Path(
    "churn_confusion_matrix.png"
)

ROC_FILE = Path(
    "churn_roc_curve.png"
)


def create_customer_data(
    sample_count: int = 1500,
    random_state: int = 2026
) -> pd.DataFrame:
    """가상의 고객 이탈 데이터를 생성합니다."""
    rng = np.random.default_rng(
        random_state
    )

    age = rng.integers(
        18,
        76,
        size=sample_count
    ).astype(float)

    monthly_fee = rng.normal(
        loc=65000,
        scale=18000,
        size=sample_count
    )

    monthly_fee = np.clip(
        monthly_fee,
        20000,
        150000
    )

    tenure_months = rng.integers(
        1,
        73,
        size=sample_count
    ).astype(float)

    support_calls = rng.poisson(
        lam=1.7,
        size=sample_count
    ).astype(float)

    contract = rng.choice(
        [
            "월간",
            "1년",
            "2년"
        ],
        size=sample_count,
        p=[
            0.55,
            0.28,
            0.17
        ]
    )

    payment_method = rng.choice(
        [
            "카드",
            "계좌이체",
            "간편결제"
        ],
        size=sample_count,
        p=[
            0.45,
            0.30,
            0.25
        ]
    )

    streaming = rng.choice(
        [
            "사용",
            "미사용"
        ],
        size=sample_count,
        p=[
            0.65,
            0.35
        ]
    )

    login_count = rng.poisson(
        lam=12,
        size=sample_count
    ).astype(float)

    linear_score = (
        -1.7
        + 0.000018
        * (
            monthly_fee
            - 65000
        )
        - 0.035
        * tenure_months
        + 0.36
        * support_calls
        - 0.025
        * login_count
        + 0.95
        * (
            contract == "월간"
        )
        - 0.75
        * (
            contract == "2년"
        )
        + 0.30
        * (
            payment_method
            == "계좌이체"
        )
        + 0.25
        * (
            streaming
            == "미사용"
        )
    )

    churn_probability = (
        1
        / (
            1
            + np.exp(
                -linear_score
            )
        )
    )

    churn = rng.binomial(
        n=1,
        p=churn_probability
    )

    data = pd.DataFrame({
        "나이": age,
        "월요금": monthly_fee,
        "가입기간": tenure_months,
        "문의횟수": support_calls,
        "월로그인수": login_count,
        "계약": contract,
        "결제방법": payment_method,
        "스트리밍": streaming,
        "해지": churn
    })

    # 일부 결측값을 삽입합니다.
    numeric_missing_indices = (
        rng.choice(
            data.index,
            size=50,
            replace=False
        )
    )

    category_missing_indices = (
        rng.choice(
            data.index,
            size=35,
            replace=False
        )
    )

    data.loc[
        numeric_missing_indices[:25],
        "나이"
    ] = np.nan

    data.loc[
        numeric_missing_indices[25:],
        "월요금"
    ] = np.nan

    data.loc[
        category_missing_indices,
        "결제방법"
    ] = None

    return data


def create_pipeline() -> Pipeline:
    """전처리와 분류 모델을 연결합니다."""
    numeric_columns = [
        "나이",
        "월요금",
        "가입기간",
        "문의횟수",
        "월로그인수"
    ]

    categorical_columns = [
        "계약",
        "결제방법",
        "스트리밍"
    ]

    numeric_pipeline = Pipeline([
        (
            "imputer",
            SimpleImputer(
                strategy="median"
            )
        ),
        (
            "scaler",
            StandardScaler()
        )
    ])

    categorical_pipeline = Pipeline([
        (
            "imputer",
            SimpleImputer(
                strategy="most_frequent"
            )
        ),
        (
            "encoder",
            OneHotEncoder(
                handle_unknown="ignore"
            )
        )
    ])

    preprocessor = ColumnTransformer([
        (
            "numeric",
            numeric_pipeline,
            numeric_columns
        ),
        (
            "categorical",
            categorical_pipeline,
            categorical_columns
        )
    ])

    model = LogisticRegression(
        max_iter=2000,
        class_weight="balanced",
        random_state=2026
    )

    return Pipeline([
        (
            "preprocessor",
            preprocessor
        ),
        (
            "model",
            model
        )
    ])


def evaluate_model(
    pipeline: Pipeline,
    X_test: pd.DataFrame,
    y_test: pd.Series
) -> None:
    """평가 데이터로 모델 성능을 출력합니다."""
    predictions = pipeline.predict(
        X_test
    )

    probabilities = (
        pipeline.predict_proba(
            X_test
        )[:, 1]
    )

    accuracy = accuracy_score(
        y_test,
        predictions
    )

    precision = precision_score(
        y_test,
        predictions,
        zero_division=0
    )

    recall = recall_score(
        y_test,
        predictions,
        zero_division=0
    )

    f1 = f1_score(
        y_test,
        predictions,
        zero_division=0
    )

    roc_auc = roc_auc_score(
        y_test,
        probabilities
    )

    print(
        "===== 평가 데이터 성능 ====="
    )

    print(
        f"정확도: {accuracy:.4f}"
    )

    print(
        f"정밀도: {precision:.4f}"
    )

    print(
        f"재현율: {recall:.4f}"
    )

    print(
        f"F1 점수: {f1:.4f}"
    )

    print(
        f"ROC AUC: {roc_auc:.4f}"
    )

    print()
    print(
        classification_report(
            y_test,
            predictions,
            target_names=[
                "유지",
                "해지"
            ],
            zero_division=0
        )
    )

    confusion_display = (
        ConfusionMatrixDisplay
        .from_predictions(
            y_test,
            predictions,
            display_labels=[
                "유지",
                "해지"
            ],
            cmap="Blues"
        )
    )

    confusion_display.ax_.set_title(
        "고객 이탈 혼동행렬"
    )

    plt.tight_layout()

    plt.savefig(
        CONFUSION_FILE,
        dpi=220,
        bbox_inches="tight"
    )

    plt.show()

    roc_display = (
        RocCurveDisplay
        .from_estimator(
            pipeline,
            X_test,
            y_test
        )
    )

    roc_display.ax_.set_title(
        "고객 이탈 ROC 곡선"
    )

    plt.tight_layout()

    plt.savefig(
        ROC_FILE,
        dpi=220,
        bbox_inches="tight"
    )

    plt.show()


def run_cross_validation(
    pipeline: Pipeline,
    X: pd.DataFrame,
    y: pd.Series
) -> None:
    """층화 교차검증으로 ROC AUC를 평가합니다."""
    cross_validator = StratifiedKFold(
        n_splits=5,
        shuffle=True,
        random_state=2026
    )

    scores = cross_val_score(
        pipeline,
        X,
        y,
        cv=cross_validator,
        scoring="roc_auc",
        n_jobs=-1
    )

    print(
        "===== 교차검증 ====="
    )

    print(
        f"각 Fold 점수: {scores}"
    )

    print(
        f"평균 ROC AUC: "
        f"{scores.mean():.4f}"
    )

    print(
        f"표준편차: "
        f"{scores.std():.4f}"
    )


def print_coefficients(
    pipeline: Pipeline
) -> None:
    """로지스틱 회귀의 주요 계수를 출력합니다."""
    preprocessor = (
        pipeline
        .named_steps[
            "preprocessor"
        ]
    )

    model = (
        pipeline
        .named_steps[
            "model"
        ]
    )

    feature_names = (
        preprocessor
        .get_feature_names_out()
    )

    coefficients = model.coef_[0]

    coefficient_table = pd.DataFrame({
        "특성": feature_names,
        "계수": coefficients,
        "절대값": np.abs(
            coefficients
        )
    })

    coefficient_table = (
        coefficient_table
        .sort_values(
            "절대값",
            ascending=False
        )
        .drop(
            columns="절대값"
        )
    )

    print(
        "===== 주요 특성 계수 ====="
    )

    print(
        coefficient_table.head(12)
    )


def predict_new_customers(
    pipeline: Pipeline
) -> None:
    """새로운 고객의 이탈 확률을 예측합니다."""
    new_customers = pd.DataFrame({
        "나이": [
            29,
            51,
            38
        ],
        "월요금": [
            115000,
            52000,
            76000
        ],
        "가입기간": [
            3,
            48,
            14
        ],
        "문의횟수": [
            6,
            1,
            3
        ],
        "월로그인수": [
            4,
            18,
            10
        ],
        "계약": [
            "월간",
            "2년",
            "1년"
        ],
        "결제방법": [
            "계좌이체",
            "카드",
            "간편결제"
        ],
        "스트리밍": [
            "미사용",
            "사용",
            "사용"
        ]
    })

    probabilities = (
        pipeline.predict_proba(
            new_customers
        )[:, 1]
    )

    predictions = pipeline.predict(
        new_customers
    )

    result = new_customers.copy()

    result["해지확률"] = (
        probabilities
    )

    result["예측결과"] = np.where(
        predictions == 1,
        "해지",
        "유지"
    )

    print(
        "===== 신규 고객 예측 ====="
    )

    print(
        result[
            [
                "가입기간",
                "월요금",
                "계약",
                "문의횟수",
                "해지확률",
                "예측결과"
            ]
        ]
    )


def main() -> None:
    data = create_customer_data()

    print(
        "===== 데이터 미리보기 ====="
    )

    print(data.head())

    print()
    print(
        "해지 클래스 비율:"
    )

    print(
        data["해지"]
        .value_counts(
            normalize=True
        )
        .sort_index()
    )

    X = data.drop(
        columns="해지"
    )

    y = data["해지"]

    X_train, X_test, y_train, y_test = (
        train_test_split(
            X,
            y,
            test_size=0.2,
            random_state=2026,
            stratify=y
        )
    )

    pipeline = create_pipeline()

    pipeline.fit(
        X_train,
        y_train
    )

    evaluate_model(
        pipeline,
        X_test,
        y_test
    )

    run_cross_validation(
        pipeline,
        X,
        y
    )

    print_coefficients(
        pipeline
    )

    predict_new_customers(
        pipeline
    )

    joblib.dump(
        pipeline,
        MODEL_FILE
    )

    print()
    print(
        f"모델 저장 위치: "
        f"{MODEL_FILE.resolve()}"
    )

    print(
        f"혼동행렬 저장 위치: "
        f"{CONFUSION_FILE.resolve()}"
    )

    print(
        f"ROC 곡선 저장 위치: "
        f"{ROC_FILE.resolve()}"
    )


if __name__ == "__main__":
    main()

프로젝트 흐름 해설

1단계: 가상 데이터 생성

data = create_customer_data()

고객의 나이, 월요금, 가입기간, 문의 횟수, 계약 방식 등을 생성합니다.

정답은 다음 열입니다.

해지:
0 → 유지
1 → 해지

2단계: 결측값 삽입

data.loc[
    indices,
    "월요금"
] = np.nan

현실적인 전처리 연습을 위해 일부 값을 비워 둡니다.

3단계: 데이터 분리

X_train, X_test, y_train, y_test = (
    train_test_split(
        X,
        y,
        test_size=0.2,
        random_state=2026,
        stratify=y
    )
)

평가 데이터는 모델 학습에 사용하지 않습니다.

4단계: 숫자 전처리

결측값을 중앙값으로 대체
→ 평균 0, 표준편차 1로 스케일링

5단계: 범주 전처리

결측값을 최빈값으로 대체
→ 원-핫 인코딩

6단계: 모델 학습

pipeline.fit(
    X_train,
    y_train
)

Pipeline이 전처리와 모델 학습을 순서대로 수행합니다.

7단계: 예측 확률

probabilities = (
    pipeline.predict_proba(
        X_test
    )[:, 1]
)

각 고객의 해지 클래스 확률을 가져옵니다.

8단계: 성능 평가

정확도
정밀도
재현율
F1 점수
ROC AUC
혼동행렬
분류 리포트

9단계: 교차검증

scores = cross_val_score(
    pipeline,
    X,
    y,
    cv=cross_validator,
    scoring="roc_auc"
)

한 번의 데이터 분할에만 의존하지 않고 여러 분할에서 성능을 확인합니다.

10단계: 신규 고객 예측

pipeline.predict_proba(
    new_customers
)

전처리 과정까지 Pipeline에 포함되어 있으므로 원본 형태의 DataFrame을 바로 전달할 수 있습니다.

고객 정보가 입구로 들어가자 결측값 처리팀, 스케일링팀, 인코딩팀, 예측팀이 차례대로 업무를 수행합니다.

머신러닝 회사의 자동화된 민원 창구가 완성되었습니다. 🏢🤖

38. 모델 저장하고 다시 불러오기

학습한 모델을 매번 다시 학습할 필요는 없습니다.

저장

import joblib

joblib.dump(
    pipeline,
    "customer_churn_model.joblib"
)

불러오기

loaded_model = joblib.load(
    "customer_churn_model.joblib"
)

예측

predictions = loaded_model.predict(
    new_customers
)

주의사항

`joblib`과 `pickle` 기반 파일은 신뢰할 수 있는 출처의 파일만 불러와야 합니다.

악의적으로 조작된 파일을 불러오면 임의 코드 실행과 같은 보안 문제가 발생할 수 있습니다.

또한 학습 환경과 불러오는 환경의 Python 및 라이브러리 버전이 다르면 호환 문제가 생길 수 있습니다. 공식 모델 저장 가이드도 사용 환경과 보안 요구에 따라 저장 방식을 선택해야 한다고 안내합니다.

모델 파일과 함께 다음 정보를 기록하는 것이 좋습니다.

Python 버전

scikit-learn 버전

NumPy 버전

pandas 버전

학습 날짜

학습 데이터 버전

특성 목록

평가 결과

모델 설정

모델 파일만 덩그러니 남겨 두면 미래의 개발자가 고고학자가 됩니다. 🏺

39. 새로운 고객 예측하기

저장한 모델을 이용해 새로운 고객을 예측해 보겠습니다.

import joblib
import pandas as pd


model = joblib.load(
    "customer_churn_model.joblib"
)


customer = pd.DataFrame({
    "나이": [34],
    "월요금": [98000],
    "가입기간": [4],
    "문의횟수": [5],
    "월로그인수": [6],
    "계약": ["월간"],
    "결제방법": ["계좌이체"],
    "스트리밍": ["미사용"]
})


prediction = model.predict(
    customer
)[0]

probability = model.predict_proba(
    customer
)[0, 1]


result = (
    "해지"
    if prediction == 1
    else "유지"
)

print(
    f"예측 결과: {result}"
)

print(
    f"해지 확률: "
    f"{probability:.2%}"
)

확률과 최종 분류의 차이

해지 확률:
0.68

기본 임계값:
0.50

최종 분류:
해지

기본 임계값은 보통 0.5이지만 비즈니스 목적에 따라 조정할 수 있습니다.

해지 고객을 최대한 놓치지 않기
→ 임계값 낮추기 검토

불필요한 할인 제공 줄이기
→ 임계값 높이기 검토

임계값을 바꾸면 정밀도와 재현율의 균형도 달라집니다.

확률은 모델의 판단 재료이고 최종 분류는 우리가 정한 정책까지 반영한 결정입니다.

40. 머신러닝 사용 시 주의점

상관관계와 인과관계를 구분한다

모델이 어떤 특성과 결과 사이의 관계를 찾았다고 해서 원인까지 증명한 것은 아닙니다.

우산 판매량 증가
↔ 교통사고 증가

우산이 교통사고를 일으킨 것이 아니라 비가 공통 원인일 수 있습니다.

학습 데이터의 편향을 확인한다

과거 데이터에 편향이 포함되어 있다면 모델도 이를 학습할 수 있습니다.

특정 지역 데이터 부족

특정 연령대 데이터 부족

과거의 불공정한 의사결정

수집 장비의 차이

운영 데이터 변화를 감시한다

시간이 지나면 데이터 특성이 달라질 수 있습니다.

고객 행동 변화

가격 정책 변경

새로운 상품 출시

경제 환경 변화

센서 교체

학습 당시 성능이 영원히 유지된다고 가정하면 안 됩니다.

평가 지표를 업무 비용과 연결한다

FP 비용:
정상 고객에게 불필요한 쿠폰 지급

FN 비용:
해지 고객을 놓침

어떤 오류가 더 비싼지에 따라 우선 지표가 달라집니다.

평가 데이터를 반복해서 사용하지 않는다

평가 결과를 보며 모델을 계속 수정하면 평가 데이터에도 간접적으로 과적합될 수 있습니다.

다음 구조를 사용할 수 있습니다.

학습 데이터
→ 모델 학습

검증 데이터
→ 모델 선택

최종 평가 데이터
→ 마지막 성능 확인

시간 순서를 고려한다

미래를 예측하는 문제에서 무작위 분할이 항상 적합한 것은 아닙니다.

2024년 데이터:
학습

2025년 데이터:
검증

2026년 데이터:
평가

모델 성능과 사업 성과는 다르다

정확도가 높아도 실제 업무 흐름과 연결되지 않으면 가치가 제한됩니다.

누가 예측 결과를 사용하는가?

언제 예측하는가?

어떤 행동으로 연결되는가?

행동의 결과를 다시 측정하는가?

개인정보와 규정을 확인한다

개인 데이터로 모델을 만들 때는 수집 목적, 보관 기간, 접근 통제, 익명화, 관련 법과 내부 정책을 확인해야 합니다.

41. 자주 발생하는 오류

오류 1. scikit-learn을 찾을 수 없음

ModuleNotFoundError:
No module named 'sklearn'

해결:

python -m pip install scikit-learn

설치 확인:

python -m pip show scikit-learn

오류 2. X가 1차원임

오류 예시:

Expected 2D array,
got 1D array instead

잘못된 입력:

X = np.array([
    1,
    2,
    3,
    4
])

수정:

X = np.array([
    [1],
    [2],
    [3],
    [4]
])

또는:

X = X.reshape(
    -1,
    1
)

오류 3. 학습과 예측 열 순서가 다름

학습:

나이
월요금
가입기간

예측:

월요금
가입기간
나이

NumPy 배열에서는 열 순서가 달라지면 잘못된 예측이 발생할 수 있습니다.

pandas DataFrame과 Pipeline을 사용하고 특성 이름을 일관되게 관리합니다.

오류 4. 문자열을 숫자 모델에 직접 전달함

ValueError:
could not convert string to float

OneHotEncoder와 ColumnTransformer를 사용합니다.

OneHotEncoder(
    handle_unknown="ignore"
)

오류 5. 결측값이 존재함

Input X contains NaN

SimpleImputer를 Pipeline에 추가합니다.

SimpleImputer(
    strategy="median"
)

오류 6. 평가 데이터에 fit_transform 사용

잘못된 예:

X_test_scaled = (
    scaler.fit_transform(
        X_test
    )
)

올바른 예:

X_test_scaled = scaler.transform(
    X_test
)

전처리 통계는 학습 데이터에서만 학습합니다.

오류 7. 새로운 범주가 등장함

Found unknown categories
during transform

다음 설정을 사용합니다.

OneHotEncoder(
    handle_unknown="ignore"
)

오류 8. 클래스가 한쪽으로 치우침

유지:
98%

해지:
2%

검토할 방법:

stratify 사용

정밀도·재현율 확인

F1 또는 ROC AUC 확인

PR AUC 확인

class_weight 검토

임계값 조정

데이터 수집 개선

오류 9. 모델이 수렴하지 않음

ConvergenceWarning

로지스틱 회귀 예:

LogisticRegression(
    max_iter=2000
)

함께 확인할 내용:

특성 스케일링

결측값

극단적인 값

solver 설정

규제 강도

경고를 무조건 숨기지 말고 원인을 확인합니다.

오류 10. 평가 점수가 실행마다 달라짐

무작위 요소가 있는 함수에 `random_state`를 지정합니다.

train_test_split(
    X,
    y,
    random_state=2026
)
RandomForestClassifier(
    random_state=2026
)

오류 11. GridSearchCV가 너무 느림

조합 수를 계산합니다.

파라미터 조합 수
× 교차검증 수
= 전체 학습 횟수

해결 방법:

탐색 범위 축소

RandomizedSearchCV 사용

cv 횟수 조정

n_jobs=-1 활용

샘플 데이터로 1차 탐색

오류 12. 모델 파일을 불러오지 못함

학습 환경과 실행 환경의 버전을 확인합니다.

import numpy
import pandas
import sklearn

print(
    numpy.__version__
)

print(
    pandas.__version__
)

print(
    sklearn.__version__
)

모델과 환경 정보를 함께 관리합니다.

42. 연습 문제

문제 1

공부 시간과 시험 점수 데이터로 선형회귀 모델을 만드세요.

X = np.array([
    [1],
    [2],
    [3],
    [4],
    [5]
])

y = np.array([
    50,
    58,
    67,
    75,
    84
])

6시간 공부했을 때 예상 점수를 출력하세요.

문제 2

다음 데이터를 학습 75%, 평가 25%로 나누세요.

train_test_split(
    X,
    y,
    test_size=0.25,
    random_state=2026
)

문제 3

회귀 모델의 MAE, RMSE, R²을 계산하세요.

from sklearn.metrics import (
    mean_absolute_error,
    r2_score,
    root_mean_squared_error
)

문제 4

붓꽃 데이터셋으로 로지스틱 회귀 분류 모델을 만드세요.

from sklearn.datasets import load_iris

평가 데이터 정확도를 출력하세요.

문제 5

붓꽃 분류 결과의 혼동행렬을 출력하세요.

confusion_matrix(
    y_test,
    predictions
)

문제 6

StandardScaler를 사용해 다음 데이터를 표준화하세요.

X = np.array([
    [20, 30000],
    [30, 50000],
    [40, 90000]
])

변환 후 각 열의 평균을 확인하세요.

문제 7

다음 범주형 데이터를 One-Hot Encoding 하세요.

contracts = np.array([
    ["월간"],
    ["1년"],
    ["2년"],
    ["월간"]
])

문제 8

다음 데이터의 결측값을 중앙값으로 대체하세요.

X = np.array([
    [10],
    [20],
    [np.nan],
    [40]
])

문제 9

StandardScaler와 LogisticRegression을 Pipeline으로 연결하세요.

pipeline = Pipeline([
    (
        "scaler",
        StandardScaler()
    ),
    (
        "model",
        LogisticRegression()
    )
])

문제 10

5겹 교차검증 정확도를 계산하세요.

scores = cross_val_score(
    pipeline,
    X,
    y,
    cv=5,
    scoring="accuracy"
)

평균과 표준편차를 출력하세요.

문제 11

RandomForestClassifier에서 다음 설정을 GridSearchCV로 탐색하세요.

parameter_grid = {
    "n_estimators": [
        100,
        300
    ],
    "max_depth": [
        None,
        5,
        10
    ]
}

문제 12

K-Means를 이용해 고객 데이터를 세 그룹으로 나누세요.

customers = np.array([
    [10, 1],
    [15, 2],
    [80, 12],
    [90, 15],
    [45, 6],
    [50, 7]
])

문제 13

불균형 분류 데이터에서 정확도가 98%인데 재현율이 0%라면 어떤 문제가 있는지 설명해 보세요.

문제 14

고객 해지 예측에서 다음 열이 데이터 누수인지 판단해 보세요.

나이
월요금
가입기간
해지처리일
문의횟수

43. 핵심 요약

설치

python -m pip install scikit-learn

데이터 분리

X_train, X_test, y_train, y_test = (
    train_test_split(
        X,
        y,
        test_size=0.2,
        random_state=2026,
        stratify=y
    )
)

회귀 모델

model = LinearRegression()

model.fit(
    X_train,
    y_train
)

predictions = model.predict(
    X_test
)

분류 모델

model = LogisticRegression(
    max_iter=1000
)

model.fit(
    X_train,
    y_train
)

회귀 평가

mean_absolute_error(
    y_test,
    predictions
)

root_mean_squared_error(
    y_test,
    predictions
)

r2_score(
    y_test,
    predictions
)

분류 평가

accuracy_score(
    y_test,
    predictions
)

precision_score(
    y_test,
    predictions
)

recall_score(
    y_test,
    predictions
)

f1_score(
    y_test,
    predictions
)

결측값 처리

SimpleImputer(
    strategy="median"
)

스케일링

StandardScaler()

범주형 인코딩

OneHotEncoder(
    handle_unknown="ignore"
)

열별 전처리

ColumnTransformer([
    (
        "numeric",
        numeric_pipeline,
        numeric_columns
    ),
    (
        "categorical",
        categorical_pipeline,
        categorical_columns
    )
])

Pipeline

pipeline = Pipeline([
    (
        "preprocessor",
        preprocessor
    ),
    (
        "model",
        LogisticRegression()
    )
])

교차검증

scores = cross_val_score(
    pipeline,
    X,
    y,
    cv=5,
    scoring="roc_auc"
)

하이퍼파라미터 탐색

search = GridSearchCV(
    pipeline,
    parameter_grid,
    cv=5
)

모델 저장

joblib.dump(
    pipeline,
    "model.joblib"
)

모델 불러오기

model = joblib.load(
    "model.joblib"
)

44. 마무리

이번 시간에는 Python의 대표 머신러닝 라이브러리 scikit-learn을 이용해 데이터에서 규칙을 학습하고 새로운 결과를 예측하는 과정을 알아보았습니다.

scikit-learn을 사용하면 다음과 같은 전체 작업을 일관된 구조로 처리할 수 있습니다.

데이터 분리

결측값 처리

숫자 스케일링

범주형 데이터 변환

회귀 모델 학습

분류 모델 학습

예측 결과 평가

교차검증

하이퍼파라미터 탐색

모델 저장

새로운 데이터 예측

머신러닝 코드의 기본 구조는 생각보다 간단합니다.

model.fit(
    X_train,
    y_train
)

predictions = model.predict(
    X_test
)

하지만 진짜 어려운 부분은 함수 이름을 외우는 것이 아닙니다.

무엇을 예측할 것인가?

올바른 데이터가 있는가?

평가 데이터가 누수되지 않았는가?

어떤 오류가 더 중요한가?

새로운 데이터에서도 잘 작동하는가?

예측 결과를 실제 업무에 어떻게 사용할 것인가?

좋은 머신러닝 프로젝트는 화려한 모델 이름에서 시작하지 않습니다.

문제를 정확하게 정의하고, 데이터를 정직하게 나누고, 알맞은 평가 지표를 선택하는 것에서 시작합니다.

나쁜 모델:
정확도 99%를 자랑하지만
중요한 사건은 하나도 찾지 못함

좋은 모델:
한계를 설명하고
실제 업무 비용에 맞는 오류를 줄임

scikit-learn은 미래를 알려 주는 예언자가 아닙니다.

과거 데이터를 꼼꼼히 읽고 새로운 사건에 가장 가능성 높은 답을 제시하는 탐정입니다.

그리고 탐정의 추리가 정확하려면 사건 기록이 깨끗해야 하고, 증거가 누출되지 않아야 하며, 결론을 검증할 독립적인 평가가 필요합니다. 🔍

이제 데이터가 등장하면 무작정 모델부터 고르지 마세요.

먼저 이렇게 질문해 보세요.

“이 문제는 회귀일까, 분류일까? 그리고 모델이 답을 미리 훔쳐보지는 않았을까?”

그 질문을 할 수 있다면 여러분은 이미 머신러닝의 가장 중요한 첫걸음을 내디딘 것입니다. 🤖📊

다음 편 예고

[Python 완전정복 시리즈 #29] TensorFlow와 Keras 완벽 이해하기 | 인공신경망이 데이터를 학습하는 방법

다음 시간에는 전통적인 머신러닝을 넘어 인공신경망과 딥러닝의 세계로 들어갑니다.

뉴런, 가중치, 편향, 활성화 함수, 손실 함수, 역전파, 에포크, 배치의 개념을 알아보고 Keras를 이용해 첫 번째 신경망 모델을 만들어 보겠습니다.

#Python #파이썬 #Python강좌 #파이썬기초 #머신러닝 #MachineLearning #scikitlearn #사이킷런 #데이터분석 #인공지능 #지도학습 #비지도학습 #회귀 #분류 #로지스틱회귀 #랜덤포레스트 #데이터전처리 #파이프라인 #교차검증 #GridSearchCV #고객이탈예측 #코딩공부 #프로그래밍

댓글

0

댓글을 불러오는 중입니다.