🧑🍳 1. 데이터 전처리란? (비유: 최고급 요리를 위한 재료 손질)
- Garbage in, garbage out (GIGO): "쓰레기를 넣으면 쓰레기가 나온다"는 뜻입니다.
- 아무리 성능이 좋은 최신 AI 모델(최고급 오븐)을 가져와도, 썩은 양파와 흙 묻은 당근(쓰레기 데이터)을 넣으면 요리(결과)는 무조건 쓰레기가 됩니다.
- 즉, 데이터 전처리는 모델이라는 오븐에 데이터를 넣기 전에 흙을 털어내고(결측치/이상치 제거), 먹기 좋게 썰고(스케일링), 껍질을 벗기는(인코딩) 아주 중요한 밑작업입니다.
🕳️ 2. 결측치(Missing Value): 빵꾸난 데이터 메우기
데이터를 엑셀로 열어봤을 때 값이 비어있는 칸(NaN, Null)들을 말합니다. 컴퓨터는 빈칸이 하나라도 있으면 에러를 뱉으며 파업해버립니다. 그래서 반드시 이걸 처리해야 합니다.
① 그냥 지워버리기 (삭제)
- df.dropna(): 빈칸이 있는 가로줄(행)을 아예 통째로 날려버립니다. (가장 쉽지만, 아까운 다른 정상 데이터도 같이 날아가는 단점이 있습니다.)
- df.dropna(axis=1): 빈칸이 너무 많아서 아예 쓸모가 없는 세로줄(기둥, 컬럼)을 통째로 뽑아버립니다.
② 그럴듯한 값으로 채워 넣기 (대체, Imputation)
비어있는 칸을 무작정 지우기 아까울 때 씁니다.
- 평균/중앙값 대체: 수치형(숫자) 데이터일 때 씁니다. 만약 데이터에 100억 부자처럼 엄청나게 튀는 값(이상치)이 섞여 있다면 '평균'은 왜곡되므로, 줄을 세웠을 때 딱 중간에 있는 사람의 값인 '중앙값(Median)'을 쓰는 것이 훨씬 안전합니다.
- 최빈값 대체: 범주형(글자) 데이터일 때 씁니다. 혈액형이 비어있다면, 사람들이 가장 많이 가지고 있는 A형으로 슬쩍 채워 넣는 식입니다.
💻 Scikit-learn의 채우기 마법사들
- SimpleImputer: "평균(mean), 중앙값(median), 최빈값(most_frequent) 중에서 하나 골라서 다 채워줘!" 라고 명령하는 가장 기본 도구입니다.
import numpy as np
import pandas as pd
from sklearn.impute import SimpleImputer
# 1. 도구(압축기) 꺼내기
# strategy="median" -> "빈칸을 평균 말고 '중앙값'으로 채울게!" 라는 설정입니다.
# (평균="mean", 최빈값="most_frequent" 로 바꿀 수 있어요)
imputer1 = SimpleImputer(strategy="median")
# 2. 기준 잡고 덮어씌우기 (학습 데이터 전용)
# A열과 B열의 데이터를 보고 중앙값을 찾은 뒤(fit), 그 값으로 빈칸을 채워라(transform)!
result1 = imputer1.fit_transform(df[['A', 'B']])
# 3. 시험지(Test) 빈칸 채울 때 (주의 🚨)
# 시험지는 새로운 기준을 찾으면 안 되므로 fit을 빼고 transform만 씁니다!
# test_result = imputer1.transform(df_test[['A', 'B']])
🖥️ 실행값 (결과):
A B
0 1 10.0
1 2 20.0
2 3 30.0 <-- (원래 빈칸이었는데, 나머지 숫자의 중앙값인 30으로 쏙 채워짐!)
3 4 40.0
4 5 50.0
- KNNImputer: 아주 똑똑한 녀석입니다. "나랑 스펙이 가장 비슷한 3명(n_neighbors=3)을 찾아보고, 그 사람들의 평균 나이로 내 빈칸을 채워줘!" 하는 방식입니다.
import numpy as np
import pandas as pd
from sklearn.impute import KNNImputer
# 1. 가상의 데이터 만들기 (세 번째 사람인 '박진우'의 나이만 비어있음)
data = {
"이름": ['김영희', '이명수', '박진우', '이수영', '오영미'],
"키": [160, 178, 175, 180, 162], # 박진우의 키는 175
"몸무게": [50, 80, 75, 85, 48], # 박진우의 몸무게는 75
"나이": [20, 30, np.nan, 32, 22] # 박진우의 나이(NaN)를 맞춰야 함!
}
df = pd.DataFrame(data)
# 머신러닝은 숫자만 계산할 수 있으므로 '이름' 컬럼은 빼고 숫자 컬럼만 가져옵니다.
df_numeric = df[['키', '몸무게', '나이']]
# -------------------------------------------------------------
# 2. KNNImputer 도구 꺼내기
# n_neighbors=2 : "나랑 스펙(키, 몸무게)이 가장 비슷한 사람 딱 2명만 찾아!"
imputer = KNNImputer(n_neighbors=2)
# 3. 빈칸 채우기 (fit: 비슷한 사람 찾기 + transform: 빈칸 채우기)
result = imputer.fit_transform(df_numeric)
# 4. 결과 확인하기 좋게 다시 판다스 표(DataFrame)로 만들기
result_df = pd.DataFrame(result, columns=['키', '몸무게', '나이'])
print(result_df)
판다스로 하나씩 채울 수도 있지만, 실무에서는 사이킷런의 SimpleImputer를 훨씬 많이 씁니다.
한 방에 여러 기둥(열)의 빈칸을 채워주거든요.
👾 3. 이상치(Outlier): 눈치 없이 튀는 녀석들 잡아내기
평균 월급이 300만 원인 동네에, 갑자기 월급 100억을 받는 빌 게이츠가 이사를 오면 평균이 확 올라가 버리겠죠? 이렇게 전체 분위기를 망치는 튀는 데이터를 '이상치'라고 합니다.
- IQR 공식으로 잡기 (가장 많이 씀): 데이터를 1등부터 100등까지 쭉 세웠을 때, 25등(1분위, Q1)과 75등(3분위, Q3) 사이의 거리를 IQR이라고 부릅니다.이 정상 범위를 벗어나는 엄청나게 작거나 큰 값들을 컴퓨터가 "너 이상치야!" 하고 걸러내는 것입니다.
- 공식은 이렇습니다: 정상 범위 = Q1 - 1.5*IQR ~ Q3 + 1.5*IQR

이상치(Outlier) 잡기: IQR 공식 코드
이 코드는 실무에서도 그대로 복붙해서 쓰는 '마법의 이상치 탐지기' 공식입니다. 외우지 마시고 흐름만 보세요!
# 'a'라는 이름의 기둥(열)에서 이상치 찾아내기
# 1. 25등(q1)과 75등(q3)의 점수를 찾습니다.
q1, q3 = df['a'].quantile(q=[0.25, 0.75])
# 2. 25등과 75등 사이의 거리(IQR)를 계산합니다.
iqr = q3 - q1
# 3. 거리(IQR)에 1.5를 곱해서 '허용해 줄 수 있는 최대 오차 범위'를 만듭니다.
iqr = iqr * 1.5
# 4. 판다스 필터링! (~ 은 '반대(Not)'라는 뜻입니다)
# "a열 데이터 중에서 (q1-iqr)부터 (q3+iqr) 사이(between)에 들어있지 않은(~) 녀석들만 가져와!"
outliers = df.loc[~df['a'].between(q1 - iqr, q3 + iqr)]
🖥️ 실행값 (결과):
a
5 1000 <-- (기계가 귀신같이 1000만 콕 집어서 이상치로 판정함!)
💡 포인트: 무작정 눈으로 튀는 값을 감으로 찾는 게 아니라, 데이터를 25등(q1)과 75등(q3)으로 줄 세운 뒤 객관적인 수치 기준으로 잘라냅니다.
🔠 4. 범주형 인코딩: 글자를 컴퓨터가 아는 '숫자'로 번역하기
컴퓨터는 'TV', '냉장고' 같은 한글을 절대 읽지 못합니다. 무조건 숫자로 바꿔줘야 합니다.
① 레이블 인코딩 (Label Encoding)
- 'TV'는 0, '냉장고'는 1, '컴퓨터'는 2... 이렇게 그냥 고유 번호표를 붙여주는 방식입니다.
- 주의사항 (별 5개 ⭐): 컴퓨터는 숫자가 크면 "어? 2번 컴퓨터가 0번 TV보다 두 배나 더 중요하고 좋은 건가?" 하고 착각하는 모델(로지스틱 회귀, SVM 등)들이 있습니다. 그래서 이 방식은 숫자 크기에 신경 안 쓰는 '트리(Tree)' 계열 모델에만 써야 합니다.
글자를 숫자로 (1) : LabelEncoder (트리 모델용)
문자로 된 정답지(y값, 예를 들어 '합격/불합격'이나 '양성/악성')를 0, 1, 2 숫자로 바꿀 때 아주 유용합니다.
from sklearn.preprocessing import LabelEncoder
# 1. 도구 꺼내기
le = LabelEncoder()
# 2. 글자 데이터를 숫자로 변환하기
# 예를 들어 df['income']에 ['High', 'Low', 'Medium']이 들어있다면 0, 1, 2로 바뀝니다.
y = le.fit_transform(df['income'])
# 3. 나중에 결과 확인할 때 (원래 글자로 되돌리기)
# 컴퓨터가 '0'이라고 예측한 걸 다시 사람이 읽기 편하게 'High'로 돌려줍니다.
original_text = le.inverse_transform([0, 1, 0])
② 원-핫 인코딩 (One-Hot Encoding)
- 위에서의 착각을 막기 위해 엑셀 기둥을 여러 개로 찢어버립니다.
- 'TV기둥', '냉장고기둥', '컴퓨터기둥'을 만들고, 자기 자신한테만 뜨겁게 불을 켜서(1), 나머지는 다 꺼버리는(0) 방식입니다. (예: TV = [1, 0, 0])
- 선형 모델이나 SVM 같은 정밀한 모델에는 무조건 원-핫 인코딩을 써야 합니다!
- 코드 소소한 팁: sparse_output=False를 안 쓰면 결과가 압축파일처럼 안 보이게 숨겨집니다. 우리가 눈으로 확인하려면 이 옵션을 꼭 넣어줘야 0과 1로 된 배열이 보입니다.
글자를 숫자로 (2) : OneHotEncoder (선형/SVM 모델용)
단어들을 각각의 기둥으로 찢어서 0과 1로만 표시하는 방법입니다.
from sklearn.preprocessing import OneHotEncoder
# 1. 도구 꺼내기
# sparse_output=False 를 꼭 써야 우리가 눈으로 볼 수 있는 일반 배열(ndarray)로 나옵니다!
ohe = OneHotEncoder(sparse_output=False)
# 2. 변환하기 (범주형 컬럼들만 모아서 쏙 집어넣습니다)
cate_features = ohe.fit_transform(df[['workclass', 'gender', 'race']])
# 3. 새로 만들어진 기둥 이름표 확인하기
# "성별_남성", "성별_여성" 처럼 기둥 이름이 어떻게 찢어졌는지 확인하는 코드입니다.
print(ohe.get_feature_names_out())
⚖️ 5. 수치형 스케일링 & 데이터 누수 (Data Leakage) 🚨
종양의 면적은 850이고, 두께는 0.05라면 단위가 달라서 컴퓨터가 또 오해를 합니다. 단위를 0~1 사이로 똑같이 맞춰주는 작업이 스케일링(Scaling)입니다.
① StandardScaler (표준화): 평균을 0으로, 표준편차(흔들림)를 1로 맞춥니다.
from sklearn.preprocessing import StandardScaler
# 1. 도구 꺼내기
s_scaler = StandardScaler()
# 2. 학습용 문제지(Train) 꾹꾹 눌러 담기 -> fit_transform 사용!
X_train_scaled = s_scaler.fit_transform(X_train)
# 3. 시험용 문제지(Test) 꾹꾹 눌러 담기 -> transform만 사용! (데이터 누수 방지)
X_test_scaled = s_scaler.transform(X_test)
Python
from sklearn.preprocessing import StandardScaler
# 1. 도구 꺼내기
s_scaler = StandardScaler()
# 2. 학습용 문제지(Train) 꾹꾹 눌러 담기 -> fit_transform 사용!
X_train_scaled = s_scaler.fit_transform(X_train)
# 3. 시험용 문제지(Test) 꾹꾹 눌러 담기 -> transform만 사용! (데이터 누수 방지)
X_test_scaled = s_scaler.transform(X_test)
② MinMaxScaler (정규화): 무조건 1등은 1, 꼴찌는 0으로 찌그러뜨립니다.
import pandas as pd
from sklearn.preprocessing import MinMaxScaler
# 가상 데이터 (단위가 엄청나게 큼)
df = pd.DataFrame({'종양크기': [100, 500, 1000]})
# 1. 도구 꺼내기
m_scaler = MinMaxScaler()
# 2. 0~1 사이로 찌그러뜨리기
df_scaled = m_scaler.fit_transform(df)
print(df_scaled)
🖥️ 실행값 (결과):
[[0. ] <-- 제일 작은 꼴찌(100)는 0이 됨
[0.4] <-- 중간치(500)는 비율에 맞춰 0.4가 됨
[1. ]] <-- 제일 큰 1등(1000)은 1이 됨
💡 포인트: 스케일링을 해도 원래 데이터의 '순위'는 절대 변하지 않습니다. 그저 단위만 0과 1 사이의 비율로 압축되어, 컴퓨터가 숫자의 크기에 압도당하는 편견을 없애줍니다.
🚨 가장 중요한 핵심: 왜 Train은 fit_transform이고, Test는 transform 인가?
- 학원 수업에서 데이터 누수(Data Leakage)라는 무서운 말을 들으셨을 겁니다.
- fit은 "평균과 1등/꼴찌 기준점을 찾아라!"라는 뜻이고, transform은 "그 기준에 맞춰 변환해라!"라는 뜻입니다.
- 기준점(fit)은 무조건 학습용 문제집(Train)에서만 찾아야 합니다. 시험지(Test)의 평균을 구해버리면 컴퓨터가 시험 문제를 풀기도 전에 힌트를 미리 훔쳐보는 것(컨닝)이 되어버립니다.
- 그래서 훈련 데이터(Train)에는 fit_transform(기준 잡고 변환!)을 하고, 시험 데이터(Test)에는 Train에서 잡은 그 기준을 그대로 가져와서 transform(변환만 해!)을 하는 것입니다.
💾 6. 모델 저장 (Pickle)
열심히 전처리하고 스케일링 기준(fit)까지 잡아놓은 똑똑한 모델과 스케일러를 컴퓨터 끄면 다 날아갑니다. 너무 아깝죠?
- import pickle을 사용해서 .pkl 확장자로 파일에 꽁꽁 싸매어(dump) 하드디스크에 저장합니다.
- 나중에 내일 컴퓨터를 켜서 그대로 불러오면(load), 어제 공부했던 똑똑한 상태 그대로 새로운 데이터(서비스)를 예측할 수 있습니다.
- wb는 Write Binary(기계어로 쓰기), rb는 Read Binary(기계어 읽기)라는 사소한 뜻이 숨어있습니다.
import os
import pickle
# 1. 저장할 폴더 만들기 (폴더가 없으면 에러가 나니까 미리 만들어줍니다)
save_dir = "saved_model"
os.makedirs(save_dir, exist_ok=True) # exist_ok=True: 폴더가 이미 있어도 에러 내지 마!
# 2. 파일 이름(경로) 짓기
model_path = os.path.join(save_dir, 'svm_model.pkl') # 결과: "saved_model/svm_model.pkl"
# 3. 진짜로 저장하기 (Dump)
# 'wb' = Write Binary (기계어로 쓴다)
with open(model_path, 'wb') as fw_model:
pickle.dump(svc2, fw_model) # 괄호 안: (저장할 모델 변수, 저장할 파일 객체)
# ---------------------------------------------
# 4. 내일 다시 컴퓨터 켜서 불러올 때 (Load)
# 'rb' = Read Binary (기계어를 읽는다)
with open(model_path, 'rb') as fr_model:
saved_svc = pickle.load(fr_model)
숨가쁘게 달려왔지만, 사실 이 긴 문서의 핵심은 "컴퓨터가 알아먹기 좋게 빵꾸 메우고, 글자는 숫자로 바꾸고, 숫자 크기는 공평하게 맞춰준다. 단, 시험지(Test) 데이터는 절대 미리 컨닝하면 안 된다!" 로 요약됩니다.
🏆 1. 결론 1: "그래서 스케일링을 하면 진짜로 성능이 좋아지나요?"
코드 후반부를 보면 똑같은 SVM(SVC) 모델을 가지고 무려 세 번이나 시험을 치는 장면이 나옵니다.
- svc1 (쌩얼 데이터): 스케일링을 전혀 안 한 원본 데이터(X_train)로 학습
- svc2 (Standard 스케일링): 평균 0, 표준편차 1로 압축한 데이터(X_train_scaled1)로 학습
- svc3 (MinMax 스케일링): 0~1 사이로 찌그러뜨린 데이터(X_train_scaled2)로 학습
선생님의 해설 💡 이 코드를 강사님이 작성하신 이유는 "스케일링의 위력"을 두 눈으로 확인시켜주기 위해서입니다. 유방암 데이터는 '종양의 크기(수백 단위)'와 '미세한 굴곡(0.001 단위)'이 섞여 있습니다. 이 데이터를 그냥(svc1) 학습시키면 정확도가 처참하게 나옵니다. 하지만 스케일링으로 숫자의 덩치를 공평하게 맞춰준 svc2나 svc3는 정확도가 95% 이상으로 껑충 뛰어오르는 기적을 보게 됩니다.
"아, SVM 모델은 숫자 단위에 엄청 예민한 녀석이라, 밥(데이터)을 주기 전에 무조건 스케일링으로 꾹꾹 눌러 담아줘야 소화를 잘 시키는구나!" 라고 깨달으시면 완벽합니다.
🚀 2. 결론 2: "저장한 모델로 '진짜 서비스'는 어떻게 하나요?"
맨 마지막 부분의 ### 서비스 주석 아래를 볼까요? 열심히 만든 모델을 저장(pickle.dump)했다가 다시 불러와서(pickle.load), 새로운 환자의 데이터(new_input)를 진단하는 실전 코스입니다.
# 1. 새로운 환자 데이터 도착! (new_input)
# 2. (핵심) 예전에 썼던 압축기(Scaler)로 똑같이 압축해 주기!
x_test_scaled = saved_scaler.transform(X_test)
# 3. 모델에게 진단(예측) 맡기기
result = saved_svc.predict(x_test_scaled)
선생님의 해설 💡 여기서 실무자들이 가장 많이 하는 실수가 나옵니다. 새로운 환자가 왔다고 해서 압축기(Scaler)를 새로 만들어서 fit을 해버리면 절대 안 됩니다! 모델은 "예전 학습 데이터의 기준(평균 등)"으로 훈련받았기 때문에, 새로운 데이터가 들어와도 무조건 "예전에 저장해 둔 똑같은 압축기(saved_scaler)"를 꺼내서 변환(transform)만 해줘야 합니다.
'두두 IT > 파이썬' 카테고리의 다른 글
| [머신런닝 5-1] 평가지표 (0) | 2026.05.14 |
|---|---|
| [머신러닝 1] 머신러닝(ML)개요 (0) | 2026.05.12 |
| [머신러닝 3-2] 데이터셋 분할 & 교차 검증 (0) | 2026.05.12 |
| [머신러닝 3-1] 데이터셋 분리와 모델 검증 (0) | 2026.05.12 |
| [머신런닝 2] 머신러닝 분석 - Iris_분석 (0) | 2026.05.12 |