파이썬 데이터 분석 #4 변형과 결측치: 새 열, 날짜, 빈 칸 다루기
CSV를 불러오자마자 바로 분석할 수 있는 데이터는 거의 없습니다. 문자열 앞뒤에 공백이 붙어 있고, 날짜가 문자열로 들어 있고, 곳곳이 비어 있고, 같은 행이 두 번 들어 있습니다. 실무에서 분석 시간의 절반 이상이 이 정리 작업에 들어갑니다. 앞 편에서 원하는 행과 열을 골라내는 법을 익혔으니, 이번 편에서는 골라낸 데이터를 분석 가능한 형태로 바꾸는 작업을 다루겠습니다. 예제 데이터에는 일부러 공백, 결측, 중복을 섞어 두었습니다.
import pandas as pd
import numpy as np
df = pd.DataFrame({
"주문번호": [1001, 1002, 1003, 1004, 1004],
"상품": [" 키보드", "마우스", "USB 허브", "모니터", "모니터"],
"단가": [49000, 15000, None, 320000, 320000],
"수량": [2, 1, 3, 1, 1],
"주문일": ["2026-04-01", "2026-04-03", "2026-04-05", "2026-04-10", "2026-04-10"],
"쿠폰": ["SUMMER10", None, "SUMMER10", None, None],
})새 열 만들기: 연산 한 줄이면 됩니다 #
단가와 수량을 곱한 금액 열을 만들어 보겠습니다. for 루프는 필요 없습니다.
df["금액"] = df["단가"] * df["수량"]
# 0 98000.0
# 1 15000.0
# 2 NaN
# 3 320000.0
# 4 320000.0df["단가"] * df["수량"]은 열 전체를 한 번에 곱합니다. 이것이 벡터화라는 사고방식입니다. 행을 하나씩 도는 파이썬 루프 대신 pandas가 내부의 C 코드로 열 전체를 한 번에 처리하므로, 백만 행이라도 같은 한 줄입니다. “행마다 어떻게 처리할까"가 아니라 “열끼리 어떤 연산을 할까"로 생각을 바꾸는 것이 pandas 적응의 핵심입니다. 결측이 있던 행은 결과도 NaN입니다. 결측은 연산을 거쳐도 결측으로 남으며, 아래에서 정리하겠습니다.
apply는 최후 수단입니다 #
조건이 들어가면 apply로 함수를 넘기고 싶어집니다.
def shipping(row):
if row["금액"] >= 100000:
return 0
return 3000
df["배송비"] = df.apply(shipping, axis=1) # 행마다 호출동작은 하지만 apply는 행마다 파이썬 함수를 호출합니다. 벡터화가 깨지고 사실상 파이썬 루프가 되어, 백만 행이면 백만 번의 함수 호출입니다. 같은 일을 벡터 연산으로 쓰면 이렇습니다.
df["배송비"] = np.where(df["금액"] >= 100000, 0, 3000)np.where(조건, 참일 때 값, 거짓일 때 값)이 열 전체에 한 번에 적용되고, 데이터가 커질수록 차이는 수십 배에서 수백 배까지 벌어집니다. 그래도 apply를 쓰는 경우는 벡터 연산으로 표현하기 어려운 복잡한 로직, 외부 라이브러리 함수를 행마다 호출해야 하는 상황 정도입니다. 벡터 연산으로 쓸 수 있으면 벡터 연산으로 쓰고, apply는 마지막에 꺼냅니다.
문자열 처리: str 액세서 #
문자열 열에는 .str를 붙여 파이썬 문자열 메서드를 열 전체에 적용합니다.
df["상품"] = df["상품"].str.strip() # 앞뒤 공백 제거
df["쿠폰"] = df["쿠폰"].str.replace("10", "") # 치환
df["상품"].str.split(" ").str[0] # 분리 후 첫 조각특히 strip은 거의 모든 실무 데이터에서 한 번은 쓰게 됩니다. “키보드"와 " 키보드"는 눈에는 같아 보여도 다른 값이라서, 공백을 정리하지 않으면 다음 편에서 다룰 그룹 집계가 둘로 갈라집니다.
날짜 처리: to_datetime과 dt 액세서 #
지금 주문일 열은 그냥 문자열입니다. pd.to_datetime으로 진짜 날짜 타입(datetime64)으로 바꾸면, .dt 액세서로 연, 월, 요일을 바로 뽑을 수 있습니다.
df["주문일"] = pd.to_datetime(df["주문일"])
df["요일"] = df["주문일"].dt.day_name()
# 0 Wednesday
# 1 Friday
# 2 Sundaydt.year, dt.month, dt.day도 같은 방식입니다. 날짜끼리 빼면 기간이 나옵니다. df["경과일"] = (pd.Timestamp("2026-04-15") - df["주문일"]).dt.days처럼 기준일에서 빼고 .dt.days로 일수만 꺼내면 14, 12, 10, 5, 5가 나옵니다.
결측치 파악: NaN의 정체 #
비어 있는 값은 pandas에서 NaN으로 표시됩니다. Not a Number의 약자로, 원래는 부동소수점 표준에 있는 특수한 float 값입니다. 그래서 정수 열에 결측이 하나라도 생기면 열 전체가 float로 바뀝니다. 위에서 금액이 98000.0처럼 소수점으로 보였던 이유입니다. NaN에는 이상한 성질도 하나 있습니다. np.nan == np.nan이 False일 정도로 자기 자신과도 같지 않습니다. 그래서 == 비교로는 결측을 찾을 수 없고, 전용 메서드 isna를 씁니다.
df.isna().sum()
# 단가 1
# 쿠폰 3
# 나머지 열은 전부 0isna()는 결측 여부를 True와 False로 돌려주고, sum()이 True를 1로 세어 열별 결측 개수가 나옵니다. 새 데이터를 받으면 가장 먼저 돌려볼 한 줄입니다.
채울까 지울까: dropna와 fillna #
결측 처리는 지우거나 채우거나 둘 중 하나입니다.
df.dropna() # 결측이 하나라도 있는 행 전부 제거
df.dropna(subset=["단가"]) # 단가가 빈 행만 제거
df["쿠폰"] = df["쿠폰"].fillna("미사용") # 결측을 지정한 값으로 채움어느 쪽을 고를지는 그 빈 칸의 의미에 달려 있습니다. 쿠폰의 결측은 “쿠폰을 쓰지 않았다"라는 정보라서 “미사용"으로 채우는 것이 맞습니다. 반면 단가의 결측은 값을 모르는 것이라서, 0으로 채우면 평균 단가와 합계 금액이 전부 왜곡됩니다. 0과 결측은 다른 값입니다. 0은 “값이 0이다"라는 사실이고, 결측은 “값을 모른다"라는 상태입니다. 평균으로 채우는 방법도 자주 보입니다만, 평균 대치는 데이터의 흩어진 정도를 실제보다 작게 만들고 결측이 많을수록 분포가 평균 쪽으로 쏠립니다. 결측 비율이 낮고 단순 요약이 목적일 때만 가볍게 쓰고, 그 외에는 지우는 쪽이 안전한 경우가 많습니다.
타입 변경: astype #
단가를 다시 정수로 되돌리고 싶어도, NaN이 남아 있는 동안은 정수 변환이 에러를 냅니다. NaN 자체가 float라서 정수 열에 들어갈 수 없기 때문입니다. 결측을 정리한 뒤에 변환합니다.
df = df.dropna(subset=["단가"])
df["단가"] = df["단가"].astype("int64") # float64 → int64
df["상품"] = df["상품"].astype("category") # 카테고리형값의 종류가 몇 가지로 정해진 문자열 열은 마지막 줄처럼 카테고리형으로 바꿔두면 좋습니다. 같은 문자열을 반복 저장하는 대신 내부적으로 번호표를 매겨 저장하기 때문에, 메모리가 크게 줄고 그룹 집계도 빨라집니다. 수백만 행에 상품명이 수십 종류뿐인 데이터라면 효과가 확실합니다.
중복 제거: duplicated와 drop_duplicates #
주문번호 1004가 두 번 들어 있었습니다. duplicated로 확인하고 drop_duplicates로 제거합니다.
df.duplicated().sum() # 1
df = df.drop_duplicates()duplicated는 모든 열의 값이 완전히 같은 행을 중복으로 봅니다. 특정 열 기준으로만 보려면 subset=["주문번호"]처럼 지정하고, 기본은 첫 행을 남기지만 keep="last"로 마지막 행을 남길 수도 있습니다. 중복을 모르고 합계를 내면 매출이 부풀려지니, 결측 집계와 함께 분석 초반에 한 번 확인하는 습관이 필요합니다.
정리 #
이번 편에서 다룬 내용입니다.
- 새 열은 열 간 연산 한 줄로 만들고, 행 루프 대신 벡터화로 생각합니다
apply는 행마다 파이썬 함수를 호출해 느리므로 최후 수단으로 남깁니다.str액세서로 공백 제거, 치환, 분리를 열 전체에 적용합니다to_datetime으로 날짜 타입을 만들고.dt로 연, 월, 요일, 날짜 차이를 꺼냅니다- 결측은
isna().sum()으로 파악하고, 빈 칸의 의미에 따라dropna와fillna를 고릅니다 astype으로 타입을 정리하고, 반복 문자열은 카테고리형으로 바꾸고,drop_duplicates로 중복 행을 제거합니다
다음 편(#5 그룹과 집계·결합)에서는 정리된 데이터를 실제로 요약하는 단계로 넘어가겠습니다. groupby로 그룹별 합계와 평균을 내고, 여러 표를 merge와 concat으로 이어 붙이는 방법까지 다루겠습니다.