서론: 왜 데이터 전처리가 가장 중요한가
데이터 과학에서 모델링보다 더 많은 시간이 걸리는 단계가 있습니다. 바로 데이터 전처리(Data Preprocessing)입니다. 실무 데이터 사이언티스트들의 업무 시간 중 약 60~80%가 데이터 수집, 정제, 변환에 소비된다는 통계는 이미 유명하죠. 솔직히, 저도 처음엔 이 수치가 과장이라고 생각했습니다. 그런데 실제로 프로젝트를 몇 번 경험하고 나니… 정말이더라고요.
아무리 좋은 머신러닝 알고리즘을 사용해도 입력 데이터의 품질이 낮으면 결과를 신뢰할 수 없습니다. "Garbage In, Garbage Out"이라는 원칙은 2026년에도 여전히 유효합니다.
이 가이드에서는 pandas 3.0을 기반으로 실무에서 자주 마주하는 데이터 전처리 기법들을 다룹니다. 결측값 처리, 이상치 탐지 및 제거, 데이터 타입 변환, 중복 제거, 텍스트 정제, 날짜 데이터 처리까지 — 실행 가능한 코드 예제와 함께 설명하겠습니다. pandas 3.0의 Copy-on-Write와 새로운 str dtype을 활용한 최신 기법도 포함되어 있으니, 기존 pandas 사용자라면 성능 향상을 직접 체감할 수 있을 겁니다.
환경 설정과 샘플 데이터 준비
pandas 3.0 설치 확인
이 가이드의 모든 코드는 pandas 3.0 이상에서 테스트되었습니다. 먼저 환경부터 확인해봅시다.
import pandas as pd
import numpy as np
print(pd.__version__) # 3.0.x 이상 확인
# PyArrow 백엔드 확인 (문자열 성능 최적화에 필요)
import pyarrow
print(pyarrow.__version__)
pandas 3.0이 아직 설치되지 않았다면 아래 명령으로 업그레이드하면 됩니다.
pip install pandas>=3.0 pyarrow
실습용 샘플 데이터셋 생성
자, 그럼 실제 현업에서 만날 법한 "지저분한" 데이터를 만들어보겠습니다. 이커머스 고객 주문 데이터를 가정할 건데, 일부러 온갖 문제를 집어넣었습니다.
import pandas as pd
import numpy as np
np.random.seed(42)
n = 1000
data = {
"order_id": range(1, n + 1),
"customer_name": np.random.choice(
["김철수", "이영희", "박민수", " 최지은 ", "정대호", None, "김철수", ""],
n
),
"email": np.random.choice(
["[email protected]", "[email protected]", "[email protected]",
"invalid-email", None, "[email protected]"], n
),
"age": np.random.choice(
[25, 30, 35, -5, 150, None, 28, 42, 33, 99999], n
),
"order_amount": np.random.choice(
[15000, 32000, None, 89000, -1000, 0, 45000, 1500000, 23000], n
),
"order_date": np.random.choice(
["2026-01-15", "2026/02/20", "15-03-2026", "2026-04-10",
"invalid_date", None, "2026-06-01"], n
),
"category": np.random.choice(
["전자제품", "의류", "식품", "전자 제품", "의류 ", " 식품", None], n
),
"rating": np.random.choice(
[1.0, 2.5, 3.0, 4.5, 5.0, None, -1.0, 6.0, 3.5], n
),
}
df = pd.DataFrame(data)
# 의도적으로 중복 행 추가
duplicate_rows = df.sample(50, random_state=42)
df = pd.concat([df, duplicate_rows], ignore_index=True)
print(f"데이터 크기: {df.shape}")
print(f"\n데이터 타입:\n{df.dtypes}")
print(f"\n처음 5행:\n{df.head()}")
보시다시피, 결측값, 이상치, 중복, 불일치한 형식, 잘못된 값 등 실무에서 흔히 마주하는 문제가 전부 들어있습니다. (나이가 -5세인 고객이라니, 현실에서도 가끔 이런 데이터를 만나면 한숨이 나옵니다.)
1단계: 데이터 탐색 — 문제 진단하기
전처리를 바로 시작하고 싶은 마음은 이해하지만, 먼저 데이터 상태를 정확히 파악하는 게 순서입니다. 저는 이 단계를 "데이터 건강 검진"이라고 부르는데, 진짜로 병원 검진처럼 꼼꼼하게 해야 합니다.
기본 정보 확인
# 전체 구조 파악
print(df.info())
# 수치형 열의 기술 통계
print(df.describe())
# 각 열의 결측값 수와 비율
missing = df.isnull().sum()
missing_pct = (missing / len(df) * 100).round(2)
missing_report = pd.DataFrame({
"결측값 수": missing,
"결측 비율(%)": missing_pct
}).sort_values("결측 비율(%)", ascending=False)
print(missing_report[missing_report["결측값 수"] > 0])
데이터 품질 체크리스트
아래 항목들은 어떤 데이터 전처리 프로젝트든 첫 번째로 확인해야 할 것들입니다. 매번 새 데이터를 받을 때마다 이 리스트를 꺼내 봅니다.
- 결측값: 어떤 열에 얼마나 많은 결측값이 있는가?
- 중복: 동일한 행이 존재하는가?
- 데이터 타입: 각 열의 타입이 적절한가? (숫자가 문자열로 저장되어 있진 않은가?)
- 이상치: 비현실적인 값이 있는가? (나이가 -5 또는 150인 행)
- 일관성: 같은 의미의 데이터가 다른 형식으로 표현되어 있는가?
- 유효성: 데이터가 비즈니스 규칙을 만족하는가?
# 중복 행 확인
print(f"중복 행 수: {df.duplicated().sum()}")
# 각 열의 고유값 수
print(f"\n고유값 수:\n{df.nunique()}")
# 카테고리형 열의 고유값 확인 (공백, 대소문자 문제 발견)
print(f"\ncategory 고유값: {df['category'].unique()}")
print(f"email 고유값 샘플: {df['email'].dropna().unique()[:10]}")
2단계: 결측값 처리 — 상황별 최적 전략
결측값 처리는 데이터 전처리의 핵심 중 핵심입니다. 여기서 중요한 건 맹목적으로 하나의 방법만 적용하지 않는 것입니다. 열의 특성과 결측 비율에 따라 전략이 달라져야 해요.
결측 패턴 분석
# 결측 패턴 시각화를 위한 히트맵 데이터 준비
missing_matrix = df.isnull().astype(int)
# 결측값이 있는 열만 필터링
cols_with_missing = missing_matrix.columns[missing_matrix.sum() > 0]
print(f"결측값이 있는 열: {list(cols_with_missing)}")
# 결측 패턴 확인: 여러 열에서 동시에 결측이 발생하는가?
print("\n결측 패턴 (상위 10개):")
patterns = df[cols_with_missing].isnull().value_counts().head(10)
print(patterns)
전략 1: 삭제 (Deletion)
결측 비율이 매우 높은 열(70% 이상)이거나, 결측 행이 전체 데이터의 극히 일부(5% 미만)일 때 적합합니다. 사실 가장 간단한 방법이기도 하죠.
# 결측 비율이 70% 이상인 열 삭제
threshold = 0.7
high_missing_cols = missing_pct[missing_pct > threshold * 100].index.tolist()
if high_missing_cols:
df = df.drop(columns=high_missing_cols)
print(f"삭제된 열: {high_missing_cols}")
# 핵심 식별자(order_id)가 결측인 행 삭제
df = df.dropna(subset=["order_id"])
# 여러 열에 동시에 결측이 있는 행 삭제
df = df.dropna(thresh=len(df.columns) - 3) # 최소 (전체 열 수 - 3)개의 값이 있어야 유지
전략 2: 대체 (Imputation)
결측값을 합리적인 값으로 채우는 방법입니다. 열의 특성에 따라 다른 대체 전략을 써야 하는데, 이 부분을 많이들 놓치더라고요.
# 수치형: 중앙값 대체 (이상치에 덜 민감)
df["age"] = df["age"].fillna(df["age"].median())
df["order_amount"] = df["order_amount"].fillna(df["order_amount"].median())
df["rating"] = df["rating"].fillna(df["rating"].median())
# 문자열형: 최빈값 대체 또는 "미지정" 표시
df["customer_name"] = df["customer_name"].fillna("미지정")
df["category"] = df["category"].fillna("미분류")
df["email"] = df["email"].fillna("[email protected]")
# 날짜형: 전후 값으로 채우기 (시계열인 경우)
# df["order_date"] = df["order_date"].ffill() # 전방 채움
# df["order_date"] = df["order_date"].bfill() # 후방 채움
print(f"결측값 처리 후:\n{df.isnull().sum()}")
전략 3: 그룹별 대체
데이터의 맥락을 반영한 더 정교한 대체 방법입니다. 카테고리별 평균값으로 결측을 채우면 단순 전체 평균보다 훨씬 정확합니다. 개인적으로 실무에서 가장 자주 쓰는 방법이기도 합니다.
# 카테고리별 중앙값으로 order_amount 결측 대체
df["order_amount"] = df.groupby("category")["order_amount"].transform(
lambda x: x.fillna(x.median())
)
# 그룹별 대체 후에도 남은 결측값은 전체 중앙값으로 처리
df["order_amount"] = df["order_amount"].fillna(df["order_amount"].median())
pandas 3.0에서의 결측값 처리 주의사항
pandas 3.0의 Copy-on-Write 덕분에, 결측값 처리 시 실수로 원본 데이터를 손상시킬 위험이 크게 줄었습니다. 하지만 inplace=True 파라미터는 여전히 주의해서 써야 합니다.
# pandas 3.0에서 권장하는 방식: 새 변수에 할당
df_clean = df.fillna({"age": 30, "rating": 3.0})
# inplace=True 대신 재할당 사용 권장
# df.fillna({"age": 30}, inplace=True) # 가능하지만 비권장
df = df.fillna({"age": 30, "rating": 3.0}) # 권장
3단계: 이상치 탐지 및 처리
이상치(Outlier)는 데이터의 일반적인 패턴에서 벗어난 극단적인 값입니다. 통계 분석과 머신러닝 모델 성능에 큰 영향을 미치므로, 그냥 넘어갈 수 없는 부분이에요.
IQR(사분위수 범위) 방법
가장 널리 사용되는 이상치 탐지 기법입니다. Q1(25번째 백분위수)과 Q3(75번째 백분위수)를 기반으로 허용 범위를 설정하죠.
def detect_outliers_iqr(series, multiplier=1.5):
"""IQR 방법으로 이상치를 탐지합니다."""
Q1 = series.quantile(0.25)
Q3 = series.quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - multiplier * IQR
upper = Q3 + multiplier * IQR
outliers = (series < lower) | (series > upper)
return outliers, lower, upper
# age 열의 이상치 탐지
age_outliers, age_lower, age_upper = detect_outliers_iqr(df["age"])
print(f"age 이상치 수: {age_outliers.sum()}")
print(f"허용 범위: {age_lower:.0f} ~ {age_upper:.0f}")
# order_amount 열의 이상치 탐지
amount_outliers, amt_lower, amt_upper = detect_outliers_iqr(df["order_amount"])
print(f"\norder_amount 이상치 수: {amount_outliers.sum()}")
print(f"허용 범위: {amt_lower:,.0f} ~ {amt_upper:,.0f}")
Z-Score 방법
데이터가 정규분포를 따른다고 가정할 때 유용합니다. 보통 Z-Score 절대값이 3을 초과하면 이상치로 봅니다.
from scipy import stats
def detect_outliers_zscore(series, threshold=3):
"""Z-Score 방법으로 이상치를 탐지합니다."""
z_scores = np.abs(stats.zscore(series.dropna()))
outlier_mask = pd.Series(False, index=series.index)
non_null_idx = series.dropna().index
outlier_mask.loc[non_null_idx] = z_scores > threshold
return outlier_mask
# Z-Score 기반 이상치 탐지
amount_zscore_outliers = detect_outliers_zscore(df["order_amount"])
print(f"Z-Score 기반 order_amount 이상치 수: {amount_zscore_outliers.sum()}")
비즈니스 규칙 기반 이상치 처리
솔직히 말하면, 통계적 방법만으로는 부족할 때가 꽤 많습니다. 도메인 지식을 활용한 비즈니스 규칙이 오히려 더 정확할 수 있어요. "나이가 -5세"를 굳이 IQR로 잡을 필요가 있을까요?
# 비즈니스 규칙 기반 필터링
# age: 0세 미만 또는 120세 초과는 비현실적
df.loc[(df["age"] < 0) | (df["age"] > 120), "age"] = np.nan
# order_amount: 음수 주문금액은 비논리적
df.loc[df["order_amount"] < 0, "order_amount"] = np.nan
# rating: 1~5 범위를 벗어나는 값 처리
df.loc[(df["rating"] < 1) | (df["rating"] > 5), "rating"] = np.nan
# 이상치 처리 후 결측값 재대체
df["age"] = df["age"].fillna(df["age"].median())
df["order_amount"] = df["order_amount"].fillna(df["order_amount"].median())
df["rating"] = df["rating"].fillna(df["rating"].median())
print("비즈니스 규칙 적용 후 기술 통계:")
print(df[["age", "order_amount", "rating"]].describe())
이상치 캡핑(Capping / Winsorizing)
이상치를 완전히 제거하는 대신, 허용 범위의 경계값으로 대체하는 방법도 있습니다. 데이터를 잃지 않으면서 극단값의 영향을 줄일 수 있어서 실무에서 꽤 유용합니다.
def cap_outliers(series, lower_pct=0.01, upper_pct=0.99):
"""백분위수 기반으로 이상치를 캡핑합니다."""
lower = series.quantile(lower_pct)
upper = series.quantile(upper_pct)
return series.clip(lower=lower, upper=upper)
# order_amount 캡핑
df["order_amount"] = cap_outliers(df["order_amount"])
print(f"캡핑 후 order_amount 범위: {df['order_amount'].min():,.0f} ~ {df['order_amount'].max():,.0f}")
4단계: 데이터 타입 변환과 형식 통일
올바른 데이터 타입은 메모리 효율과 연산 성능에 직접적으로 영향을 미칩니다. pandas 3.0에서는 타입 시스템이 크게 개선되어서 더 정확한 타입 관리가 가능해졌습니다.
수치형 데이터 최적화
# 정수형 다운캐스팅으로 메모리 절약
print(f"변환 전 메모리: {df.memory_usage(deep=True).sum() / 1024:.1f} KB")
# age를 적절한 정수형으로 변환
df["age"] = pd.to_numeric(df["age"], errors="coerce").astype("Int32")
# order_id는 음수가 없으므로 unsigned 정수 사용 가능
df["order_id"] = df["order_id"].astype("UInt32")
print(f"변환 후 메모리: {df.memory_usage(deep=True).sum() / 1024:.1f} KB")
날짜 데이터 통일
날짜 데이터는 형식이 제각각인 경우가 정말 많습니다. (같은 팀원이 입력한 데이터인데도 형식이 다른 경우를 여러 번 봤습니다.) pd.to_datetime()을 활용해서 일관된 형식으로 바꿔줍시다.
# 다양한 형식의 날짜를 자동 파싱
df["order_date"] = pd.to_datetime(df["order_date"], errors="coerce", dayfirst=False)
# 파싱 실패한 행 확인
invalid_dates = df[df["order_date"].isna()]
print(f"유효하지 않은 날짜 수: {len(invalid_dates)}")
# 날짜에서 유용한 특성 추출
df["order_year"] = df["order_date"].dt.year
df["order_month"] = df["order_date"].dt.month
df["order_dayofweek"] = df["order_date"].dt.day_name()
print(df[["order_date", "order_year", "order_month", "order_dayofweek"]].head())
카테고리형 변환으로 메모리 절약
반복되는 문자열 값이 많은 열은 category 타입으로 변환하면 메모리를 크게 아낄 수 있습니다. 특히 고유값이 몇 개 안 되는 열에서 효과가 큽니다.
# category 열을 카테고리 타입으로 변환
print(f"변환 전 category 메모리: {df['category'].memory_usage(deep=True) / 1024:.1f} KB")
df["category"] = df["category"].astype("category")
print(f"변환 후 category 메모리: {df['category'].memory_usage(deep=True) / 1024:.1f} KB")
5단계: 텍스트 데이터 정제
텍스트 데이터는 전처리에서 가장 손이 많이 가는 부분입니다. 다행히 pandas 3.0의 새로운 str dtype과 PyArrow 백엔드 덕분에 텍스트 정제 작업이 이전보다 훨씬 빨라졌습니다.
공백 및 대소문자 통일
# 앞뒤 공백 제거
df["customer_name"] = df["customer_name"].str.strip()
df["category"] = df["category"].str.strip()
# 이메일 소문자 통일
df["email"] = df["email"].str.lower()
# 빈 문자열을 결측값으로 변환
df["customer_name"] = df["customer_name"].replace("", np.nan)
print(f"정제 후 category 고유값: {df['category'].unique()}")
카테고리 이름 표준화
같은 의미인데 다르게 표기된 값들을 통일하는 작업입니다. 이거 안 하면 나중에 집계할 때 골치 아파집니다.
# 매핑 테이블을 이용한 표준화
category_mapping = {
"전자 제품": "전자제품",
"전자 제품": "전자제품",
"의류 ": "의류",
" 식품": "식품",
}
df["category"] = df["category"].replace(category_mapping)
print(f"표준화 후 category 고유값: {sorted(df['category'].dropna().unique())}")
이메일 유효성 검증
import re
def is_valid_email(email):
"""간단한 이메일 유효성 검증"""
if pd.isna(email):
return False
pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
return bool(re.match(pattern, str(email)))
# 이메일 유효성 검증
df["email_valid"] = df["email"].apply(is_valid_email)
invalid_emails = df[~df["email_valid"]]
print(f"유효하지 않은 이메일 수: {len(invalid_emails)}")
print(f"유효하지 않은 이메일 샘플:\n{invalid_emails['email'].unique()[:5]}")
# 유효하지 않은 이메일 처리
df.loc[~df["email_valid"], "email"] = np.nan
df = df.drop(columns=["email_valid"])
6단계: 중복 데이터 제거
중복 데이터는 분석 결과를 왜곡합니다. 집계나 모델 학습 시 동일한 데이터가 여러 번 반영되면 편향이 생기니까요.
완전 중복 제거
# 중복 행 확인
print(f"전체 행 수: {len(df)}")
print(f"중복 행 수: {df.duplicated().sum()}")
# 중복 제거 (첫 번째 행 유지)
df = df.drop_duplicates(keep="first")
print(f"중복 제거 후 행 수: {len(df)}")
특정 열 기준 중복 제거
# order_id 기준 중복 제거 (같은 주문이 여러 번 기록된 경우)
print(f"order_id 기준 중복: {df.duplicated(subset=['order_id']).sum()}")
df = df.drop_duplicates(subset=["order_id"], keep="first")
print(f"order_id 기준 중복 제거 후: {len(df)}")
유사 중복 탐지
완전히 동일하지는 않지만 사실상 같은 데이터인 경우를 잡아내는 건 좀 더 까다롭습니다. 텍스트 유사도를 활용하면 됩니다.
# 고객명 기반 유사 중복 탐지 (공백 제거 후 비교)
df["name_normalized"] = df["customer_name"].str.replace(r"\s+", "", regex=True)
name_duplicates = df[df.duplicated(subset=["name_normalized", "order_amount"], keep=False)]
print(f"유사 중복 후보 수: {len(name_duplicates)}")
# 정리
df = df.drop(columns=["name_normalized"])
7단계: 전처리 파이프라인 자동화
여기까지 배운 기법들을 재사용 가능한 함수로 묶어봅시다. 실무에서는 같은 전처리를 반복적으로 적용해야 하니까, 파이프라인화는 거의 필수입니다.
전처리 함수 모듈화
def preprocess_orders(df):
"""이커머스 주문 데이터 전처리 파이프라인"""
df = df.copy() # 원본 보호 (pandas 3.0에서도 명시적 복사 권장)
# 1. 중복 제거
df = df.drop_duplicates(keep="first")
# 2. 텍스트 정제
text_cols = df.select_dtypes(include=["str", "object"]).columns
for col in text_cols:
df[col] = df[col].str.strip()
df[col] = df[col].replace("", np.nan)
# 3. 카테고리 표준화
if "category" in df.columns:
mapping = {"전자 제품": "전자제품", "의류 ": "의류", " 식품": "식품"}
df["category"] = df["category"].replace(mapping)
# 4. 이메일 소문자 변환
if "email" in df.columns:
df["email"] = df["email"].str.lower()
# 5. 날짜 변환
if "order_date" in df.columns:
df["order_date"] = pd.to_datetime(df["order_date"], errors="coerce")
# 6. 이상치 처리 (비즈니스 규칙)
if "age" in df.columns:
df.loc[(df["age"] < 0) | (df["age"] > 120), "age"] = np.nan
if "order_amount" in df.columns:
df.loc[df["order_amount"] < 0, "order_amount"] = np.nan
if "rating" in df.columns:
df.loc[(df["rating"] < 1) | (df["rating"] > 5), "rating"] = np.nan
# 7. 결측값 대체
numeric_cols = df.select_dtypes(include=["number"]).columns
for col in numeric_cols:
df[col] = df[col].fillna(df[col].median())
string_cols = df.select_dtypes(include=["str", "object"]).columns
for col in string_cols:
df[col] = df[col].fillna("미지정")
return df
# 파이프라인 실행
df_clean = preprocess_orders(df)
print(f"전처리 전: {df.shape}")
print(f"전처리 후: {df_clean.shape}")
print(f"\n결측값:\n{df_clean.isnull().sum()}")
전처리 리포트 생성
전처리가 끝났으면 전후 비교 리포트를 만들어두는 게 좋습니다. 나중에 "데이터 왜 이렇게 줄었어?"라는 질문이 들어왔을 때 대비할 수 있거든요.
def generate_cleaning_report(original, cleaned):
"""전처리 전후 비교 리포트를 생성합니다."""
report = {
"원본 행 수": len(original),
"정제 후 행 수": len(cleaned),
"제거된 행 수": len(original) - len(cleaned),
"제거 비율(%)": round((len(original) - len(cleaned)) / len(original) * 100, 2),
"원본 결측값 총 수": original.isnull().sum().sum(),
"정제 후 결측값 총 수": cleaned.isnull().sum().sum(),
"원본 중복 행 수": original.duplicated().sum(),
"정제 후 중복 행 수": cleaned.duplicated().sum(),
}
report_df = pd.DataFrame(list(report.items()), columns=["항목", "값"])
return report_df
report = generate_cleaning_report(df, df_clean)
print(report.to_string(index=False))
8단계: 대규모 데이터 전처리 성능 최적화
수백만 건 이상의 데이터를 다룰 때는 성능 최적화를 무시할 수 없습니다. pandas 3.0의 새 기능들과 함께 실용적인 팁 몇 가지를 소개하겠습니다.
벡터화 연산 활용
apply() 대신 벡터화된 연산을 사용하면 성능이 수십 배 향상됩니다. 이건 정말 체감이 확 되는 부분이에요.
import time
n_large = 500_000
large_df = pd.DataFrame({
"value": np.random.randn(n_large),
"category": np.random.choice(["A", "B", "C"], n_large),
"text": [f"데이터_{i}" for i in range(n_large)]
})
# 느린 방법: apply() 사용
start = time.time()
result_slow = large_df["value"].apply(lambda x: x * 2 + 1 if x > 0 else x * 0.5)
print(f"apply() 소요 시간: {time.time() - start:.4f}초")
# 빠른 방법: 벡터화 연산
start = time.time()
result_fast = np.where(large_df["value"] > 0, large_df["value"] * 2 + 1, large_df["value"] * 0.5)
print(f"벡터화 소요 시간: {time.time() - start:.4f}초")
청크 단위 처리
메모리에 한 번에 올리기 어려운 대용량 CSV 파일은 청크 단위로 나눠서 처리할 수 있습니다.
# 대용량 CSV를 청크 단위로 읽고 전처리
def process_large_csv(filepath, chunksize=100_000):
"""대용량 CSV 파일을 청크 단위로 전처리합니다."""
chunks = []
for chunk in pd.read_csv(filepath, chunksize=chunksize):
# 각 청크에 전처리 적용
cleaned = preprocess_orders(chunk)
chunks.append(cleaned)
return pd.concat(chunks, ignore_index=True)
# 사용 예시
# df_large = process_large_csv("large_orders.csv", chunksize=50_000)
메모리 사용량 모니터링
def memory_report(df):
"""DataFrame의 열별 메모리 사용량을 보고합니다."""
mem = df.memory_usage(deep=True)
total_mb = mem.sum() / 1024 / 1024
report = pd.DataFrame({
"열 이름": mem.index,
"메모리 (KB)": (mem.values / 1024).round(2),
"비율 (%)": (mem.values / mem.sum() * 100).round(2)
}).sort_values("메모리 (KB)", ascending=False)
print(f"총 메모리 사용량: {total_mb:.2f} MB")
print(report.to_string(index=False))
return report
memory_report(df_clean)
실전 팁: 자주 발생하는 전처리 실수와 해결법
데이터 전처리를 수년간 하다 보면 반복적으로 보이는 실수 패턴이 있습니다. 이 함정들을 미리 알면 디버깅 시간을 확실히 줄일 수 있습니다.
실수 1: 전처리 순서를 무시하는 것
전처리에는 올바른 순서가 있습니다. 결측값을 먼저 처리한 후 이상치를 탐지해야 하고, 타입 변환은 텍스트 정제 이후에 하는 게 맞습니다. 순서를 무시하면 예상치 못한 에러가 발생합니다.
# 올바른 전처리 순서
# 1. 중복 제거 → 2. 텍스트 정제 → 3. 타입 변환 → 4. 결측값 처리 → 5. 이상치 처리
# 잘못된 순서: 타입 변환을 먼저 하면 "invalid_date" 같은 값에서 오류 발생
실수 2: 학습/테스트 데이터 분리 전에 전처리하기
머신러닝 프로젝트에서 정말 자주 보는 실수입니다. 전체 데이터에 대해 통계치를 계산하면 데이터 누수(Data Leakage)가 발생해요. 이렇게 되면 모델 성능이 부풀려져서, 실제 배포 후에 성능이 급격히 떨어지는 원인이 됩니다.
from sklearn.model_selection import train_test_split
# 잘못된 방법: 전체 데이터로 평균 계산 후 채우기
# df["age"] = df["age"].fillna(df["age"].mean()) # 데이터 누수!
# 올바른 방법: 학습 데이터의 통계만 사용
X_train, X_test = train_test_split(df_clean, test_size=0.2, random_state=42)
train_age_mean = X_train["age"].mean()
X_train["age"] = X_train["age"].fillna(train_age_mean)
X_test["age"] = X_test["age"].fillna(train_age_mean) # 학습 데이터의 평균 사용
실수 3: 문자열 인코딩 문제 무시하기
한국어 데이터를 다룰 때 특히 자주 겪는 문제입니다. Windows에서 만든 CSV 파일을 Mac이나 Linux에서 열면 깨지는 경우가 많죠.
# CSV 파일 읽을 때 인코딩 지정
# df = pd.read_csv("data.csv", encoding="utf-8") # 기본값
# df = pd.read_csv("data.csv", encoding="cp949") # 한국어 Windows 파일
# df = pd.read_csv("data.csv", encoding="euc-kr") # 레거시 한국어 인코딩
# 인코딩 자동 감지 (chardet 라이브러리 사용)
# import chardet
# with open("data.csv", "rb") as f:
# result = chardet.detect(f.read(10000))
# print(result["encoding"])
전처리 체크리스트 총정리
마지막으로, 모든 데이터 전처리 프로젝트에서 활용할 수 있는 체크리스트입니다. 저도 새 프로젝트를 시작할 때마다 이 목록을 한 번씩 훑어봅니다.
- 데이터 탐색:
info(),describe(),isnull().sum()으로 전체 상태 파악 - 중복 제거:
drop_duplicates()로 완전/부분 중복 제거 - 텍스트 정제: 공백 제거, 대소문자 통일, 표기 표준화
- 데이터 타입 변환: 수치형, 날짜형, 카테고리형 올바르게 변환
- 결측값 처리: 삭제, 대체, 그룹별 대체 중 상황에 맞는 전략 선택
- 이상치 처리: IQR, Z-Score, 비즈니스 규칙 기반 탐지 및 처리
- 유효성 검증: 이메일, 전화번호 등 형식 검증
- 파이프라인 구축: 재사용 가능한 함수로 자동화
- 리포트 생성: 전처리 전후 비교 문서화
자주 묻는 질문 (FAQ)
결측값이 전체 데이터의 몇 퍼센트 이상이면 해당 열을 삭제해야 하나요?
일반적으로 70% 이상이면 삭제를 고려합니다. 다만 이건 절대적인 기준이 아닙니다. 해당 열이 분석 목적에 핵심적이라면 50% 결측이더라도 보존하고 적절한 대체 전략을 적용해야 합니다. 반대로 분석에 크게 필요 없는 열이라면 30%만 결측이어도 과감히 삭제할 수 있어요. 결국 도메인 지식과 분석 목적에 따라 판단해야 합니다.
평균값 대체와 중앙값 대체 중 어떤 것을 사용해야 하나요?
이상치가 있을 가능성이 있다면 중앙값을 쓰세요. 평균은 극단값에 민감하기 때문에, 소수의 이상치가 대체값을 크게 왜곡할 수 있습니다. 예를 들어 소득 데이터에서 상위 1%의 고소득자가 평균을 끌어올리면, 대부분의 결측값이 실제보다 높게 채워지겠죠. 데이터가 정규분포에 가깝고 이상치가 없다면 평균도 괜찮습니다.
pandas 3.0에서 데이터 전처리 시 주의할 점은 무엇인가요?
가장 큰 변화는 Copy-on-Write가 기본 동작이 된 것입니다. 체인 할당(df["col"][mask] = value)이 더 이상 작동하지 않으므로, 반드시 df.loc[mask, "col"] = value 형태를 사용해야 합니다. 그리고 문자열 열이 자동으로 str dtype으로 추론되기 때문에, dtype == "object"로 문자열 열을 식별하던 기존 코드는 수정이 필요합니다. 새 str dtype은 PyArrow 백엔드를 사용해서 문자열 연산이 5~10배 빨라졌으니, 텍스트 전처리 성능 향상을 체감할 수 있을 겁니다.
이상치를 제거하는 것과 캡핑하는 것 중 어떤 방법이 더 나은가요?
상황에 따라 다릅니다. 명백한 오류(나이가 -5세, 음수 금액 등)는 제거하거나 결측 처리하는 것이 맞습니다. 반면 극단적이지만 실제로 가능한 값(매우 높은 주문금액 등)은 캡핑이 더 적절할 수 있어요. 캡핑은 데이터를 잃지 않으면서 극단값의 영향을 줄여주니까요. 참고로 트리 기반 모델(랜덤 포레스트, XGBoost)은 이상치에 비교적 강건해서 캡핑이 효과적이고, 선형 모델은 이상치에 민감하므로 제거가 더 나을 수 있습니다.
대용량 데이터(수천만 건 이상) 전처리에 pandas 대신 다른 도구를 써야 하나요?
pandas 3.0은 Copy-on-Write와 PyArrow 통합으로 성능이 많이 좋아졌지만, 수천만 건 이상에서는 여전히 메모리 한계에 부딪힐 수 있습니다. 이 경우 Polars(Rust 기반 고속 DataFrame), DuckDB(SQL 기반 인메모리 분석), 또는 Dask(분산 pandas)를 고려해보세요. 특히 Polars는 pandas와 API가 비슷하면서도 대규모 데이터에서 수 배~수십 배 빠릅니다. 수백만 건 이하라면 pandas 3.0으로 충분합니다.