현실 데이터의 문제점
import pandas as pd
import numpy as np
df = pd.read_csv("messy_data.csv")
df.info()
# <class 'pandas.core.frame.DataFrame'>
# RangeIndex: 1000 entries
# Data columns:
# # Column Non-Null Count Dtype
# 0 name 998 non-null object ← 2개 결측
# 1 age 950 non-null float64 ← 50개 결측
# 2 salary 1000 non-null object ← 숫자인데 object!
# 3 date 1000 non-null object ← 날짜인데 string!
결측값 탐지
# 결측값 확인
print(df.isnull().sum()) # 열별 결측 개수
print(df.isnull().mean() * 100) # 결측 비율 (%)
# 결측이 있는 행
print(df[df.isnull().any(axis=1)])
# 히트맵으로 시각화
import seaborn as sns
import matplotlib.pyplot as plt
sns.heatmap(df.isnull(), yticklabels=False, cbar=False, cmap="viridis")
plt.title("결측값 분포")
결측값 처리
# 1. 삭제
df_dropped = df.dropna() # 결측 있는 행 모두 삭제
df_dropped = df.dropna(subset=["name", "age"]) # 특정 열만 기준
df_dropped = df.dropna(thresh=3) # 3개 이상 채워진 행만 유지
# 2. 채우기 (수치형)
df["age"].fillna(df["age"].mean(), inplace=True) # 평균으로
df["age"].fillna(df["age"].median(), inplace=True) # 중앙값으로
df["age"].fillna(method="ffill", inplace=True) # 앞 값으로
# 3. 채우기 (범주형)
df["department"].fillna("미지정", inplace=True)
df["department"].fillna(df["department"].mode()[0], inplace=True) # 최빈값
# 4. 그룹별 채우기
df["salary"] = df.groupby("department")["salary"].transform(
lambda x: x.fillna(x.median())
)
데이터 타입 변환
# 문자열 → 숫자
df["salary"] = pd.to_numeric(df["salary"], errors="coerce") # 변환 실패 시 NaN
# 문자열 → 날짜
df["date"] = pd.to_datetime(df["date"], format="%Y-%m-%d")
df["date"] = pd.to_datetime(df["date"], infer_datetime_format=True)
# 날짜에서 성분 추출
df["year"] = df["date"].dt.year
df["month"] = df["date"].dt.month
df["weekday"] = df["date"].dt.day_name()
# 숫자 → 범주
df["age_group"] = pd.cut(df["age"],
bins=[0, 20, 30, 40, 100],
labels=["10대", "20대", "30대", "40대이상"])
이상값 탐지
# IQR 방법
Q1 = df["salary"].quantile(0.25)
Q3 = df["salary"].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
outliers = df[(df["salary"] < lower) | (df["salary"] > upper)]
print(f"이상값 개수: {len(outliers)}")
# Z-score 방법
from scipy import stats
z_scores = np.abs(stats.zscore(df["salary"].dropna()))
outlier_mask = z_scores > 3
print(f"3σ 이상: {outlier_mask.sum()}개")
이상값 처리
# 1. 제거
df_clean = df[(df["salary"] >= lower) & (df["salary"] <= upper)]
# 2. 클리핑 (상한/하한으로 대체)
df["salary"] = df["salary"].clip(lower=lower, upper=upper)
# 3. 로그 변환 (왜도 감소)
df["salary_log"] = np.log1p(df["salary"]) # log(1+x)
문자열 정제
# 공백 제거
df["name"] = df["name"].str.strip()
# 대소문자 통일
df["city"] = df["city"].str.lower()
# 특수문자 제거
df["phone"] = df["phone"].str.replace(r"[^0-9]", "", regex=True)
# 일관성 없는 값 통일
mapping = {"서울시": "서울", "서울특별시": "서울", "seoul": "서울"}
df["city"] = df["city"].replace(mapping)
데이터 정제 파이프라인
def clean_data(df: pd.DataFrame) -> pd.DataFrame:
df = df.copy()
# 타입 변환
df["salary"] = pd.to_numeric(df["salary"], errors="coerce")
df["date"] = pd.to_datetime(df["date"], errors="coerce")
# 문자열 정제
df["name"] = df["name"].str.strip()
# 결측값 처리
df["salary"].fillna(df["salary"].median(), inplace=True)
df.dropna(subset=["name", "date"], inplace=True)
# 이상값 클리핑
Q1, Q3 = df["salary"].quantile([0.25, 0.75])
IQR = Q3 - Q1
df["salary"] = df["salary"].clip(Q1 - 1.5*IQR, Q3 + 1.5*IQR)
return df
df_clean = clean_data(df)
print(f"정제 전: {len(df)}행, 정제 후: {len(df_clean)}행")
정리
| 문제 | 처리 방법 |
|---|---|
| 결측값 | dropna, fillna (평균/중앙값/최빈값) |
| 잘못된 타입 | pd.to_numeric, pd.to_datetime |
| 이상값 | IQR, Z-score, clip |
| 문자열 불일치 | str.strip, replace |
다음 편에서는 데이터 시각화 — Matplotlib과 Seaborn으로 인사이트를 그림으로 표현합니다.