포스트

[Python 100일 챌린지] Day 93 - 데이터 전처리 기초

[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 '기타'
)

📝 요약

  1. 결측치 처리: 제거, 평균, 중앙값
  2. 스케일링: StandardScaler(표준화), MinMaxScaler(정규화)
  3. 인코딩: LabelEncoder(순서형), OneHotEncoder(명목형)
  4. 순서: 데이터 분리 → 전처리 → 모델 학습
  5. 핵심: 테스트 데이터로는 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: 맞습니다! 이걸 “차원의 저주”라고 해요. 해결 방법:

  1. 상위 N개만 유지: 가장 많이 나오는 카테고리만 사용
    1
    2
    
    top_10 = df['도시'].value_counts().head(10).index
    df['도시'] = df['도시'].apply(lambda x: x if x in top_10 else '기타')
    
  2. Target Encoding: 각 카테고리를 평균값으로 대체 (고급 기법)

  3. 차원 축소: 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
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.