PythonPython 데이터 분석 · 3기초

데이터 정제 — 결측값, 이상값, 타입 변환

PythonPandas데이터정제결측값이상값전처리

현실 데이터의 문제점

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으로 인사이트를 그림으로 표현합니다.

궁금한 점이 있으신가요?

협업·의뢰는 아래로, 가벼운 소통은 인스타그램 @bluefox._.hi도 환영이에요.