실무 데이터 전처리를 위한 Pandas 핵심 기술 가이드

읽기 예상 시간: 9분

실습용 예제 데이터를 직접 만들고 에러를 내보며 Pandas의 핵심 전처리 기술을 빠르게 익힐 수 있어요. 문자열 형태의 날짜를 안전하게 datetime으로 변환하는 방법부터, 지저분한 예외 데이터를 어떻게 부드럽게 넘길 수 있는지 실무 관점에서 다뤄요. 또한 데이터를 자유자재로 다루기 위한 apply, map, lambda의 활용법과 대용량 데이터 처리 시 반드시 알아야 할 성능 주의사항을 짚어봐요. 마지막으로 텍스트 문자열 전처리와 여러 파일에 흩어진 데이터를 하나로 묶어내는 merge 기법까지, 현업에서 가장 빈번하게 쓰이는 Pandas 노하우를 한 번에 정리해 드릴게요.

목차

데이터 조각들이 모여 데이터프레임 구조로 조립되는 모습

실습을 위한 준비: 예제 데이터프레임 만들기

현업에서 데이터 분석을 하다 보면 처음부터 완벽하게 정제된 CSV 파일이나 엑셀 데이터를 만나는 일은 거의 없어요. 내가 작성한 전처리 로직이 맞는지, 함수가 내가 의도한 대로 정확히 동작하는지 확인하려면 아주 작고 통제된 가짜 데이터를 직접 만들어보는 습관이 필요해요. 코드를 눈으로만 대충 훑어보는 것과 내 손으로 타이핑해서 에러를 직접 마주하는 건 학습 깊이 자체가 다르거든요.

그래서 본격적인 기능 설명에 앞서, 파이썬의 기본 자료구조인 딕셔너리를 활용해 Pandas 데이터 전처리 실습용 데이터 생성 방법을 먼저 알려드릴게요. 아래 코드를 그대로 복사해서 주피터 노트북에 붙여넣고 실행해 보세요. 앞으로 다룰 모든 전처리 기술을 이 데이터 하나로 직접 테스트해 볼 수 있어요.

1
파이썬 딕셔너리로 데이터 뼈대 잡기

각 컬럼의 이름을 딕셔너리의 키(Key)로 지정하고, 그 안에 들어갈 데이터들을 리스트 형태의 값(Value)으로 쭉 나열해 줍니다.

2
DataFrame으로 변환하기

만들어진 딕셔너리를 pd.DataFrame() 함수 안에 쏙 집어넣으면 우리가 흔히 보는 엑셀 표 형태의 2차원 데이터프레임이 완성돼요.

python
create_dataframe.py
import pandas as pd

# 파이썬 딕셔너리로 컬럼별 데이터를 정의합니다.
data = {
    'Date': ['2023-01-15', '2023-01-16', '2023-01-17', '2023-13-45', '2023-01-19'],
    'Temp': [5.2, 6.1, 4.8, 7.5, 6.0],
    'Age': [25, 17, 30, 45, 12],
    'Sex': ['male', 'female', 'male', 'female', 'male'],
    'Pclass': [1, 3, 1, 2, 3],
    'SibSp': [1, 0, 1, 2, 0],
    'Name': ['Mr. John', 'Miss. Jane, Lee', 'Mr. Bob', 'Mrs. Alice', 'Master. Charlie']
}

# 딕셔너리를 Pandas DataFrame으로 변환합니다.
df = pd.DataFrame(data)
print(df)

코드 상단의 `Date` 열 데이터를 자세히 한 번 보세요. 4번째 데이터를 보면 ‘2023-13-45’라는 상식적으로 존재할 수 없는 날짜를 일부러 섞어 넣었어요. 현업의 원본 데이터에는 시스템 오류나 사람의 입력 실수로 이런 쓰레기 값(Garbage value)이 아주 징그럽게 많이 들어있거든요. 이런 지뢰 같은 값들을 스크립트가 멈추지 않게 어떻게 우회해서 처리하는지 바로 다음 섹션에서 파헤쳐 볼게요.

종이 달력이 디지털 날짜로 안전하게 변환되는 기계 장치

날짜 데이터 정복: 문자열 변환부터 예외 처리까지

외부에서 엑셀이나 CSV 파일을 불러오면 십중팔구 날짜 데이터는 컴퓨터 입장에서 그냥 단순한 글자, 즉 문자열로 인식돼요. 글자 상태로는 “가입일로부터 30일 뒤가 언제지?”, “결제일과 환불일 사이의 기간이 며칠이지?” 같은 시간 계산을 전혀 할 수가 없죠. 그래서 가장 먼저 해야 할 일은 이 문자열을 판다스가 날짜로 계산할 수 있는 `datetime` 타입으로 변환하는 작업이에요.

이때 기본적으로 사용하는 함수가 `pd.to_datetime()`이에요. 그런데 앞서 말씀드린 것처럼 데이터에 ‘2023-13-45’ 같은 불량 데이터가 하나라도 섞여 있다면 어떻게 될까요? 아무 옵션 없이 변환을 시도하면 즉시 파싱 에러(Parsing Error)를 뱉고 전체 프로그램이 그 자리에서 장렬하게 멈춰버려요. 이때 우리의 퇴근 시간을 지켜주는 마법의 치트키가 바로 판다스 날짜 다루기 예외 처리 파라미터인 `errors=’coerce’`예요.

❗ 중요

실무에서 수백만 건의 데이터를 변환할 때 errors=’coerce’ 파라미터는 선택이 아닌 필수예요. 이걸 빼먹으면 단 하나의 오타 때문에 밤새워 돌리던 배치 스크립트가 중간에 터지는 대참사를 겪을 수 있어요.

날짜 포맷을 맞춰서 안전하게 변환하고, 그 날짜에서 연도나 월만 쏙쏙 뽑아내는 과정을 코드로 확인해 볼게요.

python
datetime_conversion.py
from pandas.tseries.offsets import DateOffset

# 1. errors='coerce'로 잘못된 날짜(2023-13-45)를 NaT(결측치)로 안전하게 변환해요.
# 포맷은 %Y(4자리 연도), %m(월), %d(일)을 의미해요.
df['Date'] = pd.to_datetime(df['Date'], format='%Y-%m-%d', errors='coerce')

# 2. .dt 접근자를 사용해 날짜의 특정 부분만 뽑아내요.
df['year'] = df['Date'].dt.year       # 연도 추출
df['month'] = df['Date'].dt.month     # 월 추출
df['dayname'] = df['Date'].dt.day_name() # 요일 이름 추출

# 3. 날짜 계산하기
df['next_week'] = df['Date'] + pd.Timedelta(days=7) # 7일 뒤
df['next_month'] = df['Date'] + DateOffset(months=1) # 1개월 뒤

# 4. 특정 구간의 날짜 리스트 생성 (일 단위)
date_list = pd.date_range(start='2023-01-01', end='2023-01-05', freq='D')

`errors=’coerce’`를 넣고 코드를 실행하면 판다스는 자기가 변환할 수 없는 이상한 날짜를 만났을 때 프로그램에 화를 내는 대신 조용히 그 값을 `NaT` (Not a Time, 시간 데이터의 결측치)로 바꿔버려요. 덕분에 다음 줄 코드가 멈추지 않고 끝까지 돌아갈 수 있는 거죠. 판다스 버전이 올라가면서 월말 기준 날짜를 생성할 때 `freq=’M’` 대신 `freq=’ME’`를 써야 경고 메시지가 안 뜰 수 있으니, 버전 차이도 꼭 기억해 두세요.

돋보기로 시계열 데이터의 이동 평균 흐름을 관찰하는 모습

시계열 분석 기초: 이동 평균(rolling)과 행 이동(shift)

주식 차트 앱을 켜면 나오는 ‘5일 이동평균선’, ’20일 이동평균선’ 같은 말 많이 들어보셨죠? 시간에 따라 변하는 데이터의 추세를 읽으려면 특정 기간 동안의 평균을 구해서 노이즈를 부드럽게 깎아내는 작업이 필수예요. 또한 어제 대비 오늘 매출이 얼마나 올랐는지 증감률을 구하려면, 과거 데이터를 현재 날짜 옆으로 끌어와서 직접 빼기 연산을 해야 해요. 판다스에서는 이런 작업을 `rolling()`과 `shift()` 함수로 아주 깔끔하게 해결해요.

온도 데이터(`Temp`) 컬럼을 사용해서 최근 3일 치의 평균 온도를 구하고, 전날보다 온도가 몇 퍼센트나 변했는지 구하는 로직을 살펴볼게요.

python
time_series.py
# 1. 이동 평균 구하기 (최근 3개의 데이터를 평균 냄)
df['ma3'] = df['Temp'].rolling(window=3).mean()

# 2. 행 이동하기 (1행 위로 이동시켜 '어제 온도' 컬럼을 만듦)
df['prev_temp'] = df['Temp'].shift(1)

# 3. 전일 대비 변화율(증감률) 계산 공식
df['change_rate'] = (df['Temp'] - df['prev_temp']) / df['prev_temp']

코드를 실행하고 데이터프레임을 확인해보면 `ma3` 열의 첫 번째와 두 번째 행 값에 `NaN`(결측치)이 찍혀 있을 거예요. 이걸 보고 코드가 잘못됐다고 당황하시는 분들이 가끔 계신데, 아주 정상적인 동작이에요. `rolling(window=3)`은 무조건 과거 3개의 데이터가 모여야만 평균을 낼 수 있거든요. 첫째 날과 둘째 날은 아직 데이터가 3개가 안 모였으니 평균을 낼 수 없어서 비워두는 게 당연해요. `shift(1)` 함수 역시 데이터를 엑셀의 셀 밀어내기처럼 한 칸 밑으로 내리는 역할을 하기 때문에, 첫 번째 행의 이전 과거 값은 존재하지 않아 `NaN`이 됩니다.

📌 Note

이렇게 발생한 초기 결측치가 분석에 방해된다면 df.fillna(0)을 사용해 0으로 채우거나, df.dropna()로 해당 행을 아예 날려버리는 후처리 작업이 수반되어야 해요.

다양한 데이터 블록이 컨베이어 벨트를 거쳐 효율적으로 변환되는 과정

데이터 자유자재로 변환하기: apply, map, lambda와 성능 주의사항

데이터프레임에 있는 값들을 내 입맛에 맞게 일괄적으로 바꾸고 싶을 때, 아마 가장 먼저 떠오르는 도구가 `map()`과 `apply()`일 거예요. “남자(male)는 그냥 한글로 ‘남자’로, 여자(female)는 ‘여자’로 깔끔하게 1:1로 바꾸고 싶어!”라고 할 때는 직관적인 `map()` 함수를 쓰면 돼요. 하지만 조건이 좀 더 복잡해져서 “나이가 19살 이상이고 동시에 1등석에 탑승한 사람만 VIP로 묶어줘” 같은 다중 조건이 필요하다면 apply 함수에 사용자가 직접 만든 규칙을 태워서 실행해야 해요. 이때 짧은 일회성 규칙을 정의할 때 `lambda` 문법을 곁들여 쓰면 코드를 여러 줄 쓸 필요 없이 한 줄로 아주 세련되게 끝낼 수 있어요.

값을 조건에 맞게 치환하는 세 가지 핵심 방식을 단계별로 보여드릴게요.

python
data_transformation.py
# 1. map: 딕셔너리를 활용한 단순 1:1 치환
gender_map = {'male': '남자', 'female': '여자'}
df['Sex_kr'] = df['Sex'].map(gender_map)

# 2. apply + lambda: 한 열을 기준으로 조건 적용
# 나이가 19 이상이면 '성인', 아니면 '미성년자'를 반환하는 함수를 한 줄로 적용해요.
df['adult_yn'] = df['Age'].apply(lambda x: '성인' if x >= 19 else '미성년자')

# 3. apply (axis=1): 여러 열을 동시에 참조할 때
# 행 단위(axis=1)로 동작하며, Pclass와 SibSp 열을 동시에 확인해요.
df['filter'] = df.apply(
    lambda row: 1 if row['Pclass'] == 1 and row['SibSp'] == 1 else 0, 
    axis=1
)

코드를 보면 `apply()`는 정말 못 하는 게 없는 만능 치트키처럼 보여요. 하지만 제가 실무에서 동료들의 코드를 리뷰할 때 가장 많이 피드백을 주는 부분이기도 해요. 왜냐하면 `apply()`는 내부적으로 데이터를 한 번에 싹 처리하는 게 아니라, 1번 행부터 마지막 행까지 하나씩 순서대로 읽고 처리하는 파이썬 루프(Loop) 방식으로 굴러가거든요. 데이터가 만 건, 십만 건 단위만 돼도 속도 저하가 체감되고, 수백만 건이 넘어가는 대용량 데이터에 `apply()`를 아무 생각 없이 쓰면 코드가 끝날 때까지 밥을 먹고 와도 모자랄 만큼 연산 속도가 기하급수적으로 느려져요.

⚠️ Warning

대규모 데이터셋에서는 apply 사용을 최대한 자제하세요. 성능이 너무 느리다면 Pandas의 내장 벡터화 연산이나 Numpy의 np.where()를 최우선으로 고려하는 것이 좋습니다.

그래서 실무에서는 Pandas apply 예제와 대용량 데이터 성능 트레이드오프를 반드시 머릿속에 담고 코드를 짜야 해요. 복잡한 로직이 아니라면 Numpy의 벡터화 기능을 활용하는 것이 정답이에요.

엉켜있는 알파벳 글자들을 핀셋과 브러시로 깔끔하게 정리하는 모습

지저분한 텍스트 정리: 문자열 다루기 (str 접근자)

현업에서 텍스트 데이터를 받아보면 한숨부터 나오는 경우가 많죠? 회원 가입할 때 입력받은 주소는 띄어쓰기가 제멋대로고, 상품명에는 특수문자와 영어 대소문자가 일관성 없이 마구 섞여 있어요. 불필요한 쉼표나 기호를 지우거나 특정 단어가 들어간 행만 골라내려면 파이썬의 문자열 함수를 써야 하는데, 이걸 판다스 데이터프레임 전체에 한 번에 적용할 수 있게 해주는 것이 바로 str 접근자예요. 텍스트 열 뒤에 딱 `.str` 세 글자만 붙이면 그 뒤로 파이썬의 강력한 문자열 메서드들을 마음껏 호출할 수 있어요.

우리가 만든 예제 데이터에서 이름(`Name`) 데이터를 타겟으로 잡고, 호칭을 검색하고 쉼표를 지우고 데이터를 두 덩어리로 쪼개는 과정을 보여드릴게요.

python
string_cleaning.py
# 1. 특정 단어 포함 여부 확인 ('Mrs'가 포함된 행만 True)
df['is_mrs'] = df['Name'].str.contains('Mrs')

# 2. 불필요한 문자 치환 (쉼표를 빈 문자열로 바꿔서 제거)
df['Name_clean'] = df['Name'].str.replace(',', '')

# 3. 특정 문자를 기준으로 데이터 분리
# expand=True를 주면 분리된 결과가 리스트가 아니라 새로운 데이터프레임 열로 쪼개져요.
split_names = df['Name_clean'].str.split(' ', expand=True, n=1)
df['Title'] = split_names[0] # 첫 번째 열 (Mr, Mrs 등)
df['RealName'] = split_names[1] # 두 번째 열 (실제 이름)

# 4. 대소문자 통일
df['Title_lower'] = df['Title'].str.lower()

위 코드에서 3번째 블록에 있는 `str.split()`과 `expand=True` 파라미터 조합은 실무에서 정말 숨 쉬듯이 자주 쓰이는 기법이에요. 만약 저 `expand=True` 옵션을 빼먹으면 텍스트가 쪼개지긴 하는데, 그 결과물이 데이터프레임의 새로운 열로 깔끔하게 나뉘는 게 아니라 하나의 열 안에 파이썬 리스트(`[‘Mr.’, ‘John’]` 형태)로 뭉뚱그려져서 들어가 버려요. 나중에 이걸 다시 꺼내 쓰려면 코드가 심각하게 지저분해지거든요. 텍스트 데이터를 정리해야겠다는 생각이 들면 무조건 `.str`을 먼저 떠올리세요.

💡 Tip

split을 사용할 때 n=1 파라미터를 추가하면 기준 문자를 만났을 때 딱 한 번만 쪼개고 나머지는 그대로 통째로 둡니다. 이름 뒤에 스페이스바가 여러 번 들어간 데이터를 처리할 때 매우 안전한 방식이에요.

두 개의 다른 퍼즐 조각들이 하나의 완성된 형태로 결합되는 모습

흩어진 데이터 합치기: pd.merge()

전처리 작업의 하이라이트이자 사실상 분석의 시작점은 바로 여러 파일로 나뉜 데이터를 하나로 합치는 거예요. 고객 정보 리스트 따로, 지난주 매출 주문 내역 따로 존재하는 게 회사의 일상적인 데이터 베이스 구조잖아요. 이때 특정 공통된 열(기준키, Key)을 바탕으로 두 개의 데이터프레임을 퍼즐처럼 딱 맞게 조립해 주는 함수가 pd.merge()예요. 데이터베이스 쿼리를 짜보신 분이라면 SQL의 JOIN 명령어와 100% 동일한 개념이라고 이해하시면 한결 편하실 거예요.

실습을 위해 간단한 주문 데이터를 별도의 딕셔너리로 새롭게 만들고, 기존에 우리가 만들었던 고객 데이터프레임과 병합해 볼게요.

python
merge_data.py
# 주문 데이터를 위한 새로운 데이터프레임 생성
# 여기서 기준이 되는 열의 이름은 'customer_id'로 만들었어요.
order_data = {
    'customer_id': ['Mr. Bob', 'Mrs. Alice', 'Unknown'],
    'Order_Amount': [15000, 32000, 5000]
}
orders = pd.DataFrame(order_data)

# df의 'Name'과 orders의 'customer_id'를 기준으로 합칩니다.
# 기준 데이터(df)는 모두 남기고, 매칭되는 주문 정보만 옆에 붙이는 'left' 방식을 써요.
merged_df = pd.merge(
    df, 
    orders, 
    left_on='Name', 
    right_on='customer_id', 
    how='left'
)

병합 로직에서 가장 신경 써야 할 부분은 바로 `how` 파라미터예요. 여기에는 `inner`, `left`, `right`, `outer` 4가지 옵션을 줄 수 있는데, 실무에서 체감상 90% 이상은 `left` 조인을 써요. 왜냐하면 내가 애써 정리해 둔 메인 고객 리스트(왼쪽 데이터)는 한 명도 유실 없이 그대로 보존하면서, 그 사람들이 구매한 이력이 있다면 옆에 금액을 붙이고 없으면 결측치(NaN)로 남겨두는 게 가장 안전하고 상식적인 분석 방향이거든요. 두 데이터프레임에서 기준이 되는 열 이름이 똑같다면 `on=’컬럼명’`으로 통일하면 되지만, 위 예제처럼 열 이름이 다르면 `left_on`과 `right_on`으로 각각 명시적으로 짝을 지어주면 완벽하게 결합됩니다.

자주 묻는 질문 (FAQ)

Q. 코드를 직접 실행하며 공부하고 싶은데, 원본 데이터(df)는 어떻게 준비해야 하나요?

이 글의 첫 번째 섹션에 있는 코드를 그대로 활용하시면 돼요. 파이썬의 기본 자료구조인 딕셔너리(`{ }`) 안에 리스트(`[ ]`) 형태로 데이터를 예쁘게 정의한 뒤, `pd.DataFrame()` 함수로 한 번만 감싸주면 순식간에 실습용 테이블이 완성됩니다. 현업에서도 로직이 헷갈릴 때는 엑셀 파일을 뒤적이는 대신 이렇게 5줄짜리 미니 더미 데이터를 만들어서 함수가 어떻게 동작하는지 직관적으로 테스트하는 방식을 훨씬 많이 써요. 훨씬 빠르고 확실한 검증 방법이거든요.

Q. 날짜 변환 시 ‘2023-13-45’처럼 잘못된 날짜 값이 섞여 있어 자꾸 스크립트가 멈추고 에러가 납니다. 어떻게 해결하나요?

이건 정말 흔하게 마주치는 스트레스 요인이에요. `pd.to_datetime()` 함수 괄호 안에 `errors=’coerce’` 파라미터를 반드시 추가해 보세요. 이 옵션을 켜두면 판다스가 변환 불가능한 쓰레기 데이터를 만났을 때 빨간 에러 줄을 뱉고 프로그램을 강제 종료시키는 대신, 그 값을 `NaT`(Not a Time, 시간 결측치)라는 안전한 비어있는 값으로 조용히 덮어씌워 줘요. 덕분에 에러로 인한 스크립트 중단 없이 다음 줄 코드를 부드럽게 이어서 실행할 수 있어요.

Q. apply() 함수가 편하긴 한데, 수백만 건의 데이터를 처리하려니 연산이 너무 심각하게 느려집니다. 대안이 있을까요?

맞아요. `apply()`는 사용하긴 직관적이고 편하지만 본질적으로 데이터를 1열 종대로 세워두고 한 줄씩 순서대로 읽어 처리하는 구조(Loop)라서 대용량 데이터에서는 치명적인 속도 저하를 일으켜요. 속도를 수십 배 이상 높이려면 반복문 자체를 피해야 해요. 조건에 따라 값을 바꿀 때는 Pandas의 내장 조건 메서드를 적극 활용하거나, 아예 Numpy 라이브러리를 불러와서 C언어 기반의 초고속 벡터화(Vectorization) 연산인 `np.where()`나 `np.select()`를 사용하는 방식으로 코드를 재설계하는 것을 강력히 권장해요.

이 글이 마음에 드세요?

RSS 피드를 구독하세요!

댓글 남기기