[Python 100일 챌린지] Day 93 - 데이터 전처리 기초
데이터 전처리 = 머신러닝의 80%! 실제 데이터는 지저분합니다. 결측치, 이상치, 척도 차이… 이런 문제들을 해결하는 데이터 전처리를 마스터해봅시다. “쓰레기를 넣으면 쓰레기가 나온다(Garbage In, Garbage Out)”는 말, 오늘 체감하실 거예요!
(30분 완독 ⭐⭐⭐)
🎯 오늘의 학습 목표
📚 사전 지식
- Day 66: Pandas DataFrame 기초
- Day 92: scikit-learn 설치
- 기본적인 Pandas 문법
🎯 학습 목표 1: 결측치 처리하기
결측치란?
비어있는 데이터 (NaN, None, 빈 칸)
1
2
3
4
5
6
7
8
9
10
11
import pandas as pd
import numpy as np
# 결측치가 있는 데이터
data = {
'이름': ['철수', '영희', '민수', '지영'],
'나이': [25, np.nan, 30, 28],
'연봉': [3000, 3500, np.nan, 4000]
}
df = pd.DataFrame(data)
print(df)
출력:
1
2
3
4
5
이름 나이 연봉
0 철수 25.0 3000.0
1 영희 NaN 3500.0
2 민수 30.0 NaN
3 지영 28.0 4000.0
결측치 확인
1
2
3
4
5
6
# 결측치 개수 확인
print(df.isnull().sum())
# 결측치 비율
print("\n결측치 비율:")
print(df.isnull().sum() / len(df) * 100)
출력:
1
2
3
4
5
6
7
8
9
10
이름 0
나이 1
연봉 1
dtype: int64
결측치 비율:
이름 0.0
나이 25.0
연봉 25.0
dtype: float64
방법 1: 결측치 제거
1
2
3
4
# 결측치가 있는 행 전체 제거
df_dropped = df.dropna()
print("결측치 제거 후:")
print(df_dropped)
출력:
1
2
3
4
결측치 제거 후:
이름 나이 연봉
0 철수 25.0 3000.0
3 지영 28.0 4000.0
주의: 데이터가 너무 많이 사라질 수 있음!
방법 2: 평균값으로 채우기
1
2
3
4
5
6
7
# 숫자 컬럼의 결측치를 평균으로 채우기
df_filled = df.copy()
df_filled['나이'].fillna(df['나이'].mean(), inplace=True)
df_filled['연봉'].fillna(df['연봉'].mean(), inplace=True)
print("평균값으로 채운 후:")
print(df_filled)
출력:
1
2
3
4
5
6
평균값으로 채운 후:
이름 나이 연봉
0 철수 25.000000 3000.000000
1 영희 27.666667 3500.000000
2 민수 30.000000 3500.000000
3 지영 28.000000 4000.000000
방법 3: 중앙값으로 채우기
1
2
3
4
5
6
7
# 중앙값으로 채우기 (이상치에 덜 민감)
df_median = df.copy()
df_median['나이'].fillna(df['나이'].median(), inplace=True)
df_median['연봉'].fillna(df['연봉'].median(), inplace=True)
print("중앙값으로 채운 후:")
print(df_median)
🎯 학습 목표 2: 데이터 스케일링하기
왜 스케일링이 필요한가?
실생활 비유: 시험 점수를 생각해보세요!
- 수학 시험: 100점 만점
- 영어 에세이: 10점 만점
만약 이 두 점수를 그냥 합치면? 수학 점수가 압도적으로 영향을 줍니다! 그래서 “표준화”가 필요해요. 둘 다 같은 기준으로 맞추는 거죠.
1
2
3
4
5
6
7
8
9
10
# 척도가 다른 데이터
import pandas as pd
data = {
'나이': [25, 30, 35, 40], # 범위: 25-40 (작은 숫자)
'연봉': [3000, 4000, 5000, 6000] # 범위: 3000-6000 (큰 숫자)
}
df = pd.DataFrame(data)
print("원본 데이터:")
print(df)
문제: 연봉이 나이보다 100배 크기 때문에 머신러닝 모델이 “나이는 무시하고 연봉만 보겠다!”고 판단합니다. 😱 → 이건 마치 수학 100점, 영어 10점을 그냥 더한 것과 같아요!
StandardScaler: 표준화
평균 0, 표준편차 1로 변환
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from sklearn.preprocessing import StandardScaler
# StandardScaler 생성
scaler = StandardScaler()
# 스케일링 (fit_transform)
scaled_data = scaler.fit_transform(df)
# DataFrame으로 변환
df_scaled = pd.DataFrame(
scaled_data,
columns=df.columns
)
print("\nStandardScaler 적용 후:")
print(df_scaled)
출력:
1
2
3
4
5
6
StandardScaler 적용 후:
나이 연봉
0 -1.341641 -1.341641
1 -0.447214 -0.447214
2 0.447214 0.447214
3 1.341641 1.341641
공식: (값 - 평균) / 표준편차
MinMaxScaler: 정규화
0과 1 사이로 변환
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from sklearn.preprocessing import MinMaxScaler
# MinMaxScaler 생성
scaler = MinMaxScaler()
# 스케일링
scaled_data = scaler.fit_transform(df)
# DataFrame으로 변환
df_scaled = pd.DataFrame(
scaled_data,
columns=df.columns
)
print("MinMaxScaler 적용 후:")
print(df_scaled)
출력:
1
2
3
4
5
6
MinMaxScaler 적용 후:
나이 연봉
0 0.0 0.0
1 0.333333 0.333333
2 0.666667 0.666667
3 1.0 1.0
공식: (값 - 최솟값) / (최댓값 - 최솟값)
언제 어떤 스케일러를 사용할까?
| 스케일러 | 사용 시기 | 장점 |
|---|---|---|
| StandardScaler | 데이터가 정규분포일 때 | 이상치에 민감하지 않음 |
| MinMaxScaler | 범위를 0-1로 제한하고 싶을 때 | 직관적, 시각화 좋음 |
🎯 학습 목표 3: 범주형 데이터 인코딩하기
범주형 데이터란?
숫자가 아닌 카테고리 데이터
1
2
3
4
5
6
7
data = {
'이름': ['철수', '영희', '민수'],
'성별': ['남', '여', '남'],
'등급': ['A', 'B', 'A']
}
df = pd.DataFrame(data)
print(df)
문제: 머신러닝은 숫자만 이해합니다!
방법 1: Label Encoding
카테고리를 숫자로 변환
1
2
3
4
5
6
7
8
9
10
from sklearn.preprocessing import LabelEncoder
# LabelEncoder 생성
le = LabelEncoder()
# '성별' 인코딩
df['성별_인코딩'] = le.fit_transform(df['성별'])
print("Label Encoding 결과:")
print(df)
출력:
1
2
3
4
5
Label Encoding 결과:
이름 성별 등급 성별_인코딩
0 철수 남 A 1
1 영희 여 B 0
2 민수 남 A 1
주의: 순서가 없는 데이터에는 부적절! (남=1, 여=0이 남>여를 의미하지 않음)
방법 2: One-Hot Encoding
카테고리를 여러 개의 0/1 컬럼으로 변환
1
2
3
4
# pandas의 get_dummies 사용
df_encoded = pd.get_dummies(df, columns=['성별', '등급'])
print("One-Hot Encoding 결과:")
print(df_encoded)
출력:
1
2
3
4
5
One-Hot Encoding 결과:
이름 성별_남 성별_여 등급_A 등급_B
0 철수 1 0 1 0
1 영희 0 1 0 1
2 민수 1 0 1 0
장점: 순서 문제 해결!
scikit-learn의 OneHotEncoder
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from sklearn.preprocessing import OneHotEncoder
import numpy as np
# 데이터 준비
X = np.array([['남'], ['여'], ['남']]).reshape(-1, 1)
# OneHotEncoder 생성
encoder = OneHotEncoder(sparse_output=False)
# 인코딩
X_encoded = encoder.fit_transform(X)
print("OneHotEncoder 결과:")
print(X_encoded)
print("\n카테고리:", encoder.categories_)
출력:
1
2
3
4
5
6
OneHotEncoder 결과:
[[1. 0.]
[0. 1.]
[1. 0.]]
카테고리: [array(['남', '여'], dtype=object)]
💻 실전 예제
예제: 완전한 전처리 파이프라인
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler, LabelEncoder
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
# 1. 지저분한 데이터 생성
data = {
'나이': [25, np.nan, 35, 40, 28],
'연봉': [3000, 4000, np.nan, 6000, 3500],
'성별': ['남', '여', '남', '여', '남'],
'합격': [0, 1, 1, 1, 0]
}
df = pd.DataFrame(data)
print("원본 데이터:")
print(df)
print("\n결측치 개수:")
print(df.isnull().sum())
# 2. 결측치 처리 (평균값으로 채우기)
df['나이'].fillna(df['나이'].mean(), inplace=True)
df['연봉'].fillna(df['연봉'].mean(), inplace=True)
print("\n결측치 처리 후:")
print(df)
# 3. 범주형 데이터 인코딩
le = LabelEncoder()
df['성별_인코딩'] = le.fit_transform(df['성별'])
# 4. 특징과 레이블 분리
X = df[['나이', '연봉', '성별_인코딩']]
y = df['합격']
# 5. 스케일링
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
print("\n스케일링 후 데이터:")
print(pd.DataFrame(X_scaled, columns=X.columns))
# 6. 모델 학습 (데이터가 적어서 분리 안 함)
model = DecisionTreeClassifier(random_state=42)
model.fit(X_scaled, y)
# 7. 예측
predictions = model.predict(X_scaled)
accuracy = accuracy_score(y, predictions)
print(f"\n정확도: {accuracy * 100:.2f}%")
출력:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
원본 데이터:
나이 연봉 성별 합격
0 25.0 3000.0 남 0
1 NaN 4000.0 여 1
2 35.0 NaN 남 1
3 40.0 6000.0 여 1
4 28.0 3500.0 남 0
결측치 개수:
나이 1
연봉 1
성별 0
합격 0
dtype: int64
결측치 처리 후:
나이 연봉 성별 합격
0 25.0 3000.0 남 0
1 32.0 4000.0 여 1
2 35.0 4125.0 남 1
3 40.0 6000.0 여 1
4 28.0 3500.0 남 0
스케일링 후 데이터:
나이 연봉 성별_인코딩
0 -1.397203 -1.254465 0.0
1 0.087325 -0.332631 1.414214
2 0.610639 0.183003 0.0
3 1.483278 1.774726 1.414214
4 -0.784040 -0.370633 0.0
정확도: 100.00%
📊 전처리 체크리스트
graph TD
A[원본 데이터] --> B{결측치 있나?}
B -->|Yes| C[결측치 처리<br/>제거/평균/중앙값]
B -->|No| D{범주형 데이터?}
C --> D
D -->|Yes| E[인코딩<br/>Label/OneHot]
D -->|No| F{척도 차이?}
E --> F
F -->|Yes| G[스케일링<br/>Standard/MinMax]
F -->|No| H[모델 학습 준비 완료]
G --> H
⚠️ 주의사항
주의 1: 훈련/테스트 데이터 분리 후 스케일링
1
2
3
4
5
6
7
8
9
# ❌ 잘못된 방법
X_scaled = scaler.fit_transform(X)
X_train, X_test = train_test_split(X_scaled, ...)
# ✅ 올바른 방법
X_train, X_test = train_test_split(X, ...)
scaler.fit(X_train) # 훈련 데이터로만 fit!
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)
이유: 테스트 데이터 정보가 훈련에 새어나가면 안 됨!
주의 2: 결측치 처리 전략
1
2
3
# 결측치가 50% 이상이면 해당 컬럼 제거 고려
missing_rate = df.isnull().sum() / len(df)
print(missing_rate[missing_rate > 0.5])
주의 3: One-Hot Encoding의 차원 폭발
1
2
3
4
5
6
7
8
# 카테고리가 너무 많으면 컬럼 폭발!
# 예: 지역(시/구/동) → 수천 개 컬럼
# 해결: 상위 N개만 유지
top_categories = df['지역'].value_counts().head(10).index
df['지역'] = df['지역'].apply(
lambda x: x if x in top_categories else '기타'
)
📝 요약
- 결측치 처리: 제거, 평균, 중앙값
- 스케일링: StandardScaler(표준화), MinMaxScaler(정규화)
- 인코딩: LabelEncoder(순서형), OneHotEncoder(명목형)
- 순서: 데이터 분리 → 전처리 → 모델 학습
- 핵심: 테스트 데이터로는 fit하지 말 것!
🤔 자주 묻는 질문 (FAQ)
Q1: StandardScaler와 MinMaxScaler 중 뭘 써야 하나요?
A: 정답은 없지만 가이드라인이 있어요!
- StandardScaler 추천: 데이터가 정규분포를 따를 때, 이상치가 있을 때
- MinMaxScaler 추천: 0~1 범위가 필요할 때 (예: 이미지 픽셀)
처음엔 StandardScaler를 기본으로 쓰고, 안 되면 MinMaxScaler를 시도하세요!
Q2: 결측치를 평균으로 채우는 게 항상 좋은가요?
A: 아닙니다! 상황에 따라 달라요.
- 평균: 숫자가 정규분포일 때 (대부분의 경우)
- 중앙값: 이상치가 많을 때 (예: 연봉 데이터)
- 최빈값: 범주형 데이터일 때
- 제거: 결측치가 너무 많거나 (50% 이상) 중요하지 않을 때
팁: 여러 방법을 시도해보고 모델 성능이 좋은 것을 선택하세요!
Q3: 왜 훈련 데이터로만 fit하고 테스트 데이터는 transform만 하나요?
A: 이건 정말 중요합니다! 🚨
잘못된 방법 (데이터 누수):
1
scaler.fit(전체_데이터) # ❌ 테스트 데이터 정보까지 학습!
올바른 방법:
1
2
scaler.fit(훈련_데이터) # ✅ 훈련 데이터만 학습
scaler.transform(테스트_데이터) # ✅ 같은 규칙 적용
비유: 시험 문제를 미리 본 것과 같아요. 테스트 데이터는 “미래의 데이터”라고 생각하세요. 미래는 아직 모르잖아요?
Q4: One-Hot Encoding하면 컬럼이 너무 많아져요!
A: 맞습니다! 이걸 “차원의 저주”라고 해요. 해결 방법:
- 상위 N개만 유지: 가장 많이 나오는 카테고리만 사용
1 2
top_10 = df['도시'].value_counts().head(10).index df['도시'] = df['도시'].apply(lambda x: x if x in top_10 else '기타')
-
Target Encoding: 각 카테고리를 평균값으로 대체 (고급 기법)
- 차원 축소: PCA 같은 기법 사용 (나중에 배워요!)
팁: 카테고리가 10개 이하면 One-Hot Encoding, 그 이상이면 다른 방법을 고려하세요.
🧪 연습 문제
문제: 전처리 파이프라인 완성하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pandas as pd
import numpy as np
# 데이터
data = {
'나이': [25, 30, np.nan, 40],
'도시': ['서울', '부산', '서울', '대구'],
'합격': [0, 1, 1, 0]
}
df = pd.DataFrame(data)
# TODO:
# 1. 나이 결측치를 평균으로 채우기
# 2. 도시를 One-Hot Encoding
# 3. 나이를 StandardScaler로 스케일링
✅ 정답
1
2
3
4
5
6
7
8
9
10
11
12
13
from sklearn.preprocessing import StandardScaler
# 1. 결측치 처리
df['나이'].fillna(df['나이'].mean(), inplace=True)
# 2. One-Hot Encoding
df_encoded = pd.get_dummies(df, columns=['도시'])
# 3. 스케일링
scaler = StandardScaler()
df_encoded['나이'] = scaler.fit_transform(df_encoded[['나이']])
print(df_encoded)
📚 다음 학습
Day 94: 선형 회귀 모델 ⭐⭐⭐
내일은 드디어 첫 회귀 모델! 집값을 예측해봅니다!
“데이터 전처리가 머신러닝의 80%입니다. 나머지 20%는 모델 선택과 튜닝!” 🚀
Day 93/100 Phase 10: AI/ML 입문 #100DaysOfPython
