🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Python 데이터 정제 완벽 가이드 - 슬라이드 1/8
A

AI Generated

2025. 12. 16. · 6 Views

Python 데이터 정제 완벽 가이드

실무에서 가장 많이 사용하는 데이터 정제 기법을 처음부터 끝까지 다룹니다. 결측치 처리부터 타입 변환까지, 지저분한 데이터를 깔끔하게 만드는 방법을 배워봅니다.


목차

  1. dropna()로 결측치 제거
  2. fillna()로 결측치 대체
  3. 평균/중앙값으로 채우기
  4. astype()으로 타입 변환
  5. 문자열을 숫자로 변환
  6. 범주형 데이터 처리
  7. 정제 전후 비교하기

1. dropna()로 결측치 제거

어느 날 신입 개발자 김데이터 씨가 처음으로 실제 고객 데이터를 받았습니다. 엑셀 파일을 열어보니 곳곳에 빈 칸이 보였습니다.

"이런 데이터로 분석이 가능할까요?" 선배 데이터 분석가 박클린 씨에게 물었습니다.

**dropna()**는 데이터프레임에서 결측치(NaN, None 등)가 포함된 행이나 열을 제거하는 판다스의 기본 메서드입니다. 마치 불량품을 골라내는 품질 검사원처럼 데이터에서 문제가 있는 부분을 찾아 제거합니다.

간단하지만 강력한 이 메서드는 데이터 정제의 첫 번째 관문입니다.

다음 코드를 살펴봅시다.

import pandas as pd
import numpy as np

# 샘플 데이터 생성 - 결측치가 포함된 고객 정보
customer_data = pd.DataFrame({
    'name': ['김철수', '이영희', np.nan, '박민수'],
    'age': [25, np.nan, 35, 28],
    'email': ['kim@email.com', 'lee@email.com', 'choi@email.com', np.nan]
})

# 결측치가 하나라도 있는 행 제거
clean_data = customer_data.dropna()
print(clean_data)

# 특정 컬럼의 결측치만 고려하여 제거
partial_clean = customer_data.dropna(subset=['name'])
print(partial_clean)

김데이터 씨는 첫 실무 프로젝트를 맡았습니다. 마케팅팀에서 받은 고객 데이터 파일에는 5000개의 행이 있었는데, 자세히 보니 이름, 이메일, 나이 등이 비어있는 경우가 많았습니다.

"이걸 어떻게 처리해야 하죠?" 당황한 김데이터 씨에게 박클린 씨가 차분하게 설명했습니다. "데이터 정제의 첫 번째 단계는 결측치 처리예요.

가장 간단한 방법은 문제가 있는 행을 아예 제거하는 거죠." 결측치란 정확히 무엇일까요? 쉽게 비유하자면, 결측치는 마치 설문지에서 응답자가 건너뛴 질문과 같습니다.

"나이가 몇 살이신가요?"라는 질문에 답을 쓰지 않은 것처럼, 데이터에도 값이 없는 부분이 존재합니다. 판다스에서는 이런 값을 NaN(Not a Number)이나 None으로 표현합니다.

결측치를 그냥 두면 어떤 문제가 생길까요? 첫째, 평균이나 합계 같은 통계 계산이 제대로 되지 않습니다.

나이의 평균을 구하려는데 절반이 빈 값이라면 정확한 평균을 구할 수 없겠죠. 둘째, 머신러닝 모델은 결측치를 처리하지 못하는 경우가 많습니다.

학습을 시작하기도 전에 에러가 발생합니다. 바로 이런 문제를 해결하기 위해 **dropna()**가 등장했습니다.

dropna()를 사용하면 결측치가 포함된 행을 한 번에 제거할 수 있습니다. 기본적으로 하나의 컬럼이라도 NaN 값이 있으면 그 행 전체를 삭제합니다.

마치 불량품 검사에서 하나라도 문제가 있으면 전체를 폐기하는 것처럼 말이죠. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 샘플 데이터를 만듭니다. numpy의 np.nan을 사용해서 의도적으로 결측치를 넣었습니다.

실제 데이터에서도 이런 형태로 결측치가 나타납니다. 다음으로 **dropna()**를 호출하면 결측치가 하나라도 있는 행이 모두 제거됩니다.

원본 데이터는 4개 행이었지만, 깨끗한 데이터는 1개 행만 남게 됩니다. 하지만 여기서 박클린 씨가 중요한 점을 지적했습니다.

"모든 행을 다 제거하면 데이터가 너무 많이 사라질 수 있어요. subset 옵션을 사용하면 특정 컬럼만 검사할 수 있습니다." 실제 현업에서는 어떻게 활용할까요?

예를 들어 전자상거래 서비스를 운영한다고 가정해봅시다. 주문 데이터에서 고객 이름과 배송지 주소는 반드시 필요하지만, 전화번호는 선택사항일 수 있습니다.

이런 경우 **subset=['name', 'address']**로 지정하면 이름과 주소가 있는 데이터만 남기고, 전화번호가 없어도 그 행은 유지됩니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 무조건 dropna()로 데이터를 제거하는 것입니다. 만약 데이터의 80%에 결측치가 있다면 80%를 버리는 게 맞을까요?

귀중한 데이터를 너무 많이 잃을 수 있습니다. 따라서 먼저 결측치의 비율을 확인하고, 너무 많다면 제거 대신 대체하는 방법을 고려해야 합니다.

다시 김데이터 씨의 이야기로 돌아가 봅시다. 박클린 씨의 설명을 들은 김데이터 씨는 우선 각 컬럼의 결측치 비율을 확인했습니다.

이름 컬럼은 5%만 결측치였지만, 전화번호 컬럼은 60%나 비어있었습니다. "이름이 없는 행만 제거하고, 전화번호는 나중에 다른 방법으로 처리하겠습니다!" 김데이터 씨가 자신 있게 말했습니다.

dropna()를 제대로 이해하면 데이터 손실을 최소화하면서도 깔끔한 데이터셋을 만들 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - **how='any'**는 하나라도 결측치가 있으면 제거, **how='all'**은 모든 값이 결측치일 때만 제거합니다

  • thresh=n 옵션으로 최소 n개의 non-null 값이 있는 행만 유지할 수 있습니다
  • 제거하기 전에 **df.isna().sum()**으로 각 컬럼의 결측치 개수를 먼저 확인하세요

2. fillna()로 결측치 대체

김데이터 씨가 dropna()로 결측치를 제거했더니 5000개 데이터 중 1200개만 남았습니다. "이렇게 데이터가 적으면 분석 결과가 정확하지 않을 것 같은데요?" 박클린 씨가 미소를 지으며 말했습니다.

"그럴 땐 제거하지 말고 채워 넣으면 됩니다."

**fillna()**는 결측치를 특정 값으로 대체하는 메서드입니다. 마치 빈 칸에 연필로 값을 채워 넣듯이, NaN 값을 원하는 값으로 교체합니다.

데이터를 버리지 않고 최대한 활용할 수 있는 강력한 도구입니다.

다음 코드를 살펴봅시다.

import pandas as pd
import numpy as np

# 제품 재고 데이터 - 일부 값이 누락됨
inventory = pd.DataFrame({
    'product': ['노트북', '마우스', '키보드', '모니터'],
    'stock': [50, np.nan, 30, np.nan],
    'price': [1200000, 35000, np.nan, 450000]
})

# 결측치를 0으로 채우기
filled_with_zero = inventory.fillna(0)
print(filled_with_zero)

# 컬럼별로 다른 값으로 채우기
filled_custom = inventory.fillna({
    'stock': 0,  # 재고는 0으로
    'price': inventory['price'].mean()  # 가격은 평균으로
})
print(filled_custom)

김데이터 씨는 고민에 빠졌습니다. 5000개 중 1200개만 남으면 통계적으로 의미 있는 분석이 어려웠습니다.

특히 고객의 구매 패턴을 파악하는 프로젝트였기에 데이터가 많을수록 좋았습니다. 박클린 씨가 화면을 가리키며 설명했습니다.

"데이터를 무조건 버리는 게 능사는 아니에요. 상황에 따라 적절한 값으로 채워 넣으면 됩니다.

이걸 결측치 대체라고 합니다." 결측치 대체란 정확히 무엇일까요? 쉽게 비유하자면, 결측치 대체는 마치 빈 집에 임시 거주자를 들이는 것과 같습니다.

완벽한 해결책은 아니지만, 빈 집을 그냥 두는 것보다는 낫습니다. 데이터 분석에서도 마찬가지입니다.

결측치를 합리적인 값으로 채우면 전체 데이터를 활용할 수 있습니다. 그렇다면 어떤 값으로 채워야 할까요?

가장 간단한 방법은 0이나 특정 상수로 채우는 것입니다. 예를 들어 제품 재고 데이터에서 재고량이 비어있다면 0으로 채울 수 있습니다.

재고가 없다는 의미로 해석할 수 있기 때문입니다. 하지만 모든 경우에 0이 적절한 것은 아닙니다.

**fillna()**의 진정한 힘은 유연성에 있습니다. fillna()를 사용하면 단순히 하나의 값으로만 채우는 게 아니라, 컬럼마다 다른 전략을 사용할 수 있습니다.

재고는 0으로 채우지만, 가격은 전체 제품의 평균 가격으로 채울 수 있습니다. 이렇게 하면 데이터의 특성을 더 잘 보존할 수 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 제품 재고 데이터를 만듭니다.

stock과 price 컬럼에 의도적으로 결측치를 넣었습니다. 첫 번째 방법은 모든 결측치를 0으로 채우는 것입니다.

**fillna(0)**을 호출하면 모든 NaN이 0으로 바뀝니다. 두 번째 방법은 더 정교합니다.

딕셔너리를 사용해서 컬럼별로 다른 값을 지정합니다. 실제 현업에서는 어떻게 활용할까요?

온라인 쇼핑몰의 고객 리뷰 데이터를 분석한다고 가정해봅시다. 평점 컬럼에 결측치가 있다면 어떻게 해야 할까요?

0으로 채우면 "평점 0점"이라는 의미가 되어 부정적 평가로 왜곡됩니다. 대신 전체 평균 평점으로 채우면 더 중립적인 값이 됩니다.

이처럼 비즈니스 맥락을 고려해서 적절한 값을 선택해야 합니다. 박클린 씨가 중요한 팁을 알려주었습니다.

"method 옵션을 사용하면 앞뒤 값으로 채울 수도 있어요. **method='ffill'**은 앞의 값을 복사하고, **method='bfill'**은 뒤의 값을 복사합니다." 이 방법은 시계열 데이터에서 특히 유용합니다.

예를 들어 주식 가격 데이터에서 특정 날짜의 값이 없다면, 바로 전날의 가격을 사용하는 게 합리적입니다. 주식 가격은 급격하게 변하지 않기 때문입니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 모든 결측치를 무조건 평균으로 채우는 것입니다.

만약 데이터에 이상치(outlier)가 많다면 평균이 왜곡될 수 있습니다. 이런 경우에는 중앙값을 사용하는 게 더 안전합니다.

또한 결측치가 많은 컬럼(예: 50% 이상)을 채우는 것은 데이터를 오히려 왜곡할 수 있으므로 신중해야 합니다. 다시 김데이터 씨의 이야기로 돌아가 봅시다.

김데이터 씨는 각 컬럼의 특성을 분석한 후, 나이는 중앙값으로, 구매 횟수는 0으로, 마지막 구매일은 가장 오래된 날짜로 채우기로 결정했습니다. "이제 4800개 데이터를 분석에 사용할 수 있게 됐어요!" 김데이터 씨가 뿌듯해했습니다.

fillna()를 제대로 이해하면 데이터 손실 없이 효과적인 분석을 할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - inplace=True 옵션을 사용하면 원본 데이터프레임을 직접 수정합니다

  • limit 옵션으로 채울 결측치의 최대 개수를 제한할 수 있습니다
  • 채우기 전에 결측치가 왜 발생했는지 원인을 파악하는 게 중요합니다

3. 평균/중앙값으로 채우기

김데이터 씨가 fillna()로 결측치를 채우고 나서 분석을 시작했습니다. 그런데 결과가 이상했습니다.

고객 나이의 평균이 15살로 나왔습니다. "뭔가 잘못됐는데요?" 박클린 씨가 데이터를 살펴보더니 웃으며 말했습니다.

"결측치를 0으로 채워서 그래요."

평균중앙값으로 결측치를 채우는 것은 데이터의 분포를 유지하면서 결측치를 처리하는 통계적 방법입니다. 마치 반 평균 점수를 결석한 학생의 점수로 사용하는 것처럼, 전체 데이터의 경향을 반영한 합리적인 대체값을 사용합니다.

다음 코드를 살펴봅시다.

import pandas as pd
import numpy as np

# 고객 구매 데이터
customer_purchases = pd.DataFrame({
    'customer_id': [1, 2, 3, 4, 5, 6, 7, 8],
    'age': [25, 30, np.nan, 45, np.nan, 35, 28, 50],
    'purchase_amount': [50000, 120000, np.nan, 200000, 80000, np.nan, 150000, 1000000]
})

# 나이는 평균으로 채우기
mean_age = customer_purchases['age'].mean()
customer_purchases['age'].fillna(mean_age, inplace=True)

# 구매액은 중앙값으로 채우기 (이상치에 강건)
median_purchase = customer_purchases['purchase_amount'].median()
customer_purchases['purchase_amount'].fillna(median_purchase, inplace=True)

print(customer_purchases)

김데이터 씨는 당황했습니다. 분명히 결측치를 처리했는데 분석 결과가 현실과 동떨어졌습니다.

고객 나이의 평균이 15살이라니, 쇼핑몰 고객이 대부분 초등학생이라는 뜻일까요? 박클린 씨가 데이터를 살펴보더니 문제를 찾았습니다.

"나이 컬럼의 결측치를 0으로 채웠네요. 0살 고객이 여러 명 있으니 평균이 낮아진 거예요." 평균중앙값의 차이는 무엇일까요?

쉽게 비유하자면, 평균은 마치 반 전체의 용돈을 모아서 인원수로 나눈 값입니다. 모든 사람의 용돈을 고려하기 때문에 공정해 보입니다.

하지만 한 명이 월 1000만원을 받는다면 어떨까요? 평균이 크게 올라가서 실제 대부분 학생의 용돈과는 거리가 멀어집니다.

반면 중앙값은 모든 학생을 용돈 순서로 세웠을 때 딱 중간에 있는 학생의 값입니다. 극단적인 값의 영향을 받지 않습니다.

데이터 분석에서 왜 이런 차이가 중요할까요? 실제 비즈니스 데이터에는 이상치가 많습니다.

온라인 쇼핑몰의 구매액 데이터를 생각해봅시다. 대부분 고객은 3만원에서 10만원 정도를 구매하지만, 가끔 기업 고객이 1000만원어치를 한 번에 구매하기도 합니다.

이런 경우 평균으로 결측치를 채우면 일반 고객의 패턴과 맞지 않습니다. 바로 이런 상황에서 중앙값이 더 적절합니다.

중앙값을 사용하면 극단적인 값에 영향을 받지 않고, 대부분의 고객이 실제로 구매하는 수준의 값으로 결측치를 채울 수 있습니다. 판다스에서는 median() 메서드로 쉽게 계산할 수 있습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 고객 데이터를 만듭니다.

age와 purchase_amount에 결측치가 있습니다. 나이 컬럼은 **mean()**으로 평균을 계산하고 fillna()로 채웁니다.

구매액 컬럼은 **median()**으로 중앙값을 계산합니다. 주목할 점은 purchase_amount에 1000000이라는 이상치가 있다는 것입니다.

평균을 쓰면 왜곡되지만 중앙값은 안정적입니다. 실제 현업에서는 어떻게 활용할까요?

금융 서비스에서 고객의 신용도를 평가한다고 가정해봅시다. 연소득 데이터에 결측치가 있을 때, 평균 연소득으로 채우면 일부 고소득자 때문에 값이 높아집니다.

하지만 중앙값을 사용하면 일반적인 고객의 소득 수준을 더 잘 반영할 수 있습니다. 많은 핀테크 기업들이 이런 방식으로 데이터를 정제합니다.

박클린 씨가 추가로 설명했습니다. "언제 평균을 쓰고 언제 중앙값을 써야 하는지 헷갈리죠?

간단한 기준이 있어요." 데이터의 분포를 먼저 확인하는 것입니다. describe() 메서드로 통계 요약을 보면 평균과 중앙값이 모두 나옵니다.

이 둘의 차이가 크다면 이상치가 있다는 신호입니다. 이런 경우 중앙값이 더 안전합니다.

반대로 차이가 작다면 평균을 써도 무방합니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 모든 컬럼에 똑같은 방법을 적용하는 것입니다. 나이는 평균이 적절할 수 있지만, 구매 횟수는 중앙값이 나을 수 있습니다.

각 컬럼의 특성과 분포를 개별적으로 분석해야 합니다. 또한 결측치를 채운 후에는 반드시 분포를 다시 확인해서 원본 데이터의 패턴이 유지되는지 검증해야 합니다.

다시 김데이터 씨의 이야기로 돌아가 봅시다. 박클린 씨의 조언을 듣고 김데이터 씨는 각 컬럼의 분포를 시각화했습니다.

나이는 정규분포에 가까웠지만, 구매액은 한쪽으로 치우친 분포였습니다. "나이는 평균으로, 구매액은 중앙값으로 채우겠습니다!" 이번에는 분석 결과가 훨씬 합리적으로 나왔습니다.

평균과 중앙값의 차이를 제대로 이해하면 데이터의 특성을 보존하면서 결측치를 처리할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - **df.describe()**로 평균과 중앙값을 한 번에 확인하세요

  • 평균과 중앙값의 차이가 50% 이상이면 이상치가 있다는 신호입니다
  • **최빈값(mode)**도 범주형 데이터에서 유용한 대체값입니다

4. astype()으로 타입 변환

김데이터 씨가 결측치를 모두 처리하고 나이별 평균 구매액을 계산하려고 했습니다. 그런데 에러가 발생했습니다.

"TypeError: cannot perform operations on string and int" 박클린 씨가 데이터를 보더니 한숨을 쉬었습니다. "나이가 문자열로 저장되어 있네요."

**astype()**은 데이터프레임의 컬럼 타입을 변환하는 메서드입니다. 마치 숫자로 쓰인 문자를 실제 숫자로 바꾸는 것처럼, 데이터의 형태를 용도에 맞게 변경합니다.

올바른 타입이 있어야 제대로 된 분석이 가능합니다.

다음 코드를 살펴봅시다.

import pandas as pd

# CSV에서 불러온 데이터 - 모두 문자열로 저장됨
raw_data = pd.DataFrame({
    'user_id': ['1', '2', '3', '4'],
    'age': ['25', '30', '35', '28'],
    'is_premium': ['True', 'False', 'True', 'False'],
    'join_date': ['2024-01-15', '2024-02-20', '2024-03-10', '2024-04-05']
})

print("변환 전 타입:", raw_data.dtypes)

# 각 컬럼을 적절한 타입으로 변환
raw_data['user_id'] = raw_data['user_id'].astype(int)
raw_data['age'] = raw_data['age'].astype(int)
raw_data['is_premium'] = raw_data['is_premium'].astype(bool)
raw_data['join_date'] = pd.to_datetime(raw_data['join_date'])

print("\n변환 후 타입:", raw_data.dtypes)
print("\n나이 평균:", raw_data['age'].mean())  # 이제 계산 가능

김데이터 씨는 좌절했습니다. 결측치도 다 처리했는데 왜 계산이 안 될까요?

에러 메시지를 읽어보니 "문자열과 정수 간의 연산을 수행할 수 없다"고 나와 있었습니다. 박클린 씨가 dtypes 속성을 확인해보라고 했습니다.

"데이터프레임의 각 컬럼이 어떤 타입인지 보여주는 거예요." 확인해보니 나이 컬럼이 object 타입, 즉 문자열이었습니다. 데이터 타입이란 정확히 무엇일까요?

쉽게 비유하자면, 데이터 타입은 마치 물건을 담는 상자의 종류와 같습니다. 사과는 과일 상자에, 책은 책 상자에 담아야 합니다.

숫자 데이터는 숫자 타입에, 참거짓 값은 불린 타입에 저장해야 제대로 사용할 수 있습니다. 잘못된 상자에 담으면 나중에 꺼내 쓸 때 문제가 생깁니다.

왜 이런 일이 생길까요? 많은 경우 CSV 파일에서 데이터를 읽어올 때 모든 값이 문자열로 저장됩니다.

엑셀이나 텍스트 파일은 숫자와 문자를 구분하지 않기 때문입니다. '25'라는 값이 숫자 25인지, 문자열 "25"인지 알 수 없습니다.

판다스는 안전하게 모든 것을 문자열로 읽어옵니다. 바로 이런 문제를 해결하기 위해 **astype()**이 필요합니다.

astype()을 사용하면 컬럼의 타입을 명시적으로 변환할 수 있습니다. 문자열 '25'를 정수 25로, 문자열 'True'를 불린 True로 바꿀 수 있습니다.

일단 올바른 타입으로 변환하면 평균, 합계 같은 연산이 가능해집니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 CSV에서 불러온 것처럼 모든 값이 문자열인 데이터프레임을 만듭니다. dtypes로 확인하면 모두 object(문자열) 타입입니다.

다음으로 각 컬럼을 적절한 타입으로 변환합니다. user_id와 age는 int로, is_premium은 bool로 변환합니다.

날짜는 특별히 **pd.to_datetime()**을 사용합니다. 실제 현업에서는 어떻게 활용할까요?

전자상거래 플랫폼에서 로그 데이터를 분석한다고 가정해봅시다. 서버 로그에는 응답 시간이 문자열로 기록되어 있습니다.

"125ms", "340ms" 같은 형식이죠. 이걸 그대로 분석하면 평균을 구할 수 없습니다.

먼저 'ms'를 제거하고 정수로 변환해야 합니다. 많은 데이터 엔지니어들이 매일 이런 작업을 합니다.

박클린 씨가 중요한 팁을 알려주었습니다. "타입 변환은 데이터 정제의 핵심이에요.

하지만 조심해야 할 게 있습니다." 변환할 수 없는 값이 있으면 에러가 발생합니다. 예를 들어 나이 컬럼에 '스물다섯'이라는 한글 문자가 있다면 int로 변환할 수 없습니다.

이런 경우 errors='coerce' 옵션을 사용하면 변환 불가능한 값을 NaN으로 만들어줍니다. 또 다른 주의사항이 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 타입 변환 순서를 잘못 잡는 것입니다. 결측치를 처리하기 전에 타입을 변환하려고 하면 에러가 발생할 수 있습니다.

올바른 순서는 먼저 결측치를 처리하고, 그 다음에 타입을 변환하는 것입니다. 또한 변환 후에는 반드시 dtypes로 확인해서 의도한 대로 변환되었는지 검증해야 합니다.

다시 김데이터 씨의 이야기로 돌아가 봅시다. 김데이터 씨는 모든 컬럼의 타입을 확인하고, 하나씩 적절한 타입으로 변환했습니다.

숫자로 연산해야 하는 컬럼은 int나 float로, 참거짓 플래그는 bool로 변환했습니다. "이제 나이별 평균 구매액이 제대로 계산되네요!" 김데이터 씨가 기뻐했습니다.

astype()을 제대로 이해하면 데이터를 용도에 맞게 가공해서 정확한 분석을 할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - errors='coerce' 옵션으로 변환 실패 시 NaN으로 처리할 수 있습니다

  • 메모리 절약을 위해 int64 대신 int32, float64 대신 float32를 사용할 수 있습니다
  • 타입 변환 전에 **df.info()**로 현재 타입과 메모리 사용량을 확인하세요

5. 문자열을 숫자로 변환

김데이터 씨가 가격 컬럼을 분석하려는데 또 에러가 발생했습니다. 가격이 "1,200,000원" 형식으로 저장되어 있었습니다.

"콤마와 한글이 섞여 있어서 바로 변환이 안 되네요." 박클린 씨가 말했습니다. "이런 경우 문자열 처리가 필요해요."

문자열 전처리는 숫자가 아닌 문자가 섞인 데이터를 순수한 숫자로 변환하는 과정입니다. 마치 과일 껍질을 벗기듯이 불필요한 문자를 제거하고 숫자만 추출합니다.

실제 데이터는 깔끔한 형태가 아닌 경우가 많아 이 과정이 필수적입니다.

다음 코드를 살펴봅시다.

import pandas as pd
import numpy as np

# 실제 웹사이트에서 크롤링한 것 같은 데이터
product_data = pd.DataFrame({
    'product_name': ['노트북', '마우스', '키보드'],
    'price': ['1,200,000원', '35,000원', '89,000원'],
    'discount_rate': ['15%', '20%', '10%'],
    'stock': ['50개', '200개', '150개']
})

print("변환 전:\n", product_data)

# 가격: 콤마와 '원' 제거 후 정수로 변환
product_data['price'] = product_data['price'].str.replace(',', '').str.replace('원', '').astype(int)

# 할인율: '%' 제거 후 실수로 변환
product_data['discount_rate'] = product_data['discount_rate'].str.replace('%', '').astype(float)

# 재고: '개' 제거 후 정수로 변환
product_data['stock'] = product_data['stock'].str.replace('개', '').astype(int)

print("\n변환 후:\n", product_data)
print("\n평균 가격:", product_data['price'].mean())

김데이터 씨는 이제 데이터 정제에 익숙해졌다고 생각했습니다. 하지만 실제 웹사이트에서 크롤링한 가격 데이터를 보고 다시 당황했습니다.

"1,200,000원"을 어떻게 숫자로 바꿀까요? 박클린 씨가 웃으며 말했습니다.

"실제 데이터는 교과서처럼 깔끔하지 않아요. 사람이 읽기 편하게 포맷된 데이터를 컴퓨터가 계산할 수 있는 형태로 바꿔야 합니다." 문자열 전처리란 정확히 무엇일까요?

쉽게 비유하자면, 문자열 전처리는 마치 요리하기 전에 재료를 손질하는 것과 같습니다. 생선을 요리하려면 비늘을 벗기고 내장을 제거해야 합니다.

마찬가지로 "1,200,000원"이라는 문자열에서 콤마와 "원"을 제거하면 순수한 숫자 "1200000"이 남습니다. 이제 이걸 정수로 변환할 수 있습니다.

왜 이런 작업이 필요할까요? 웹 크롤링, 엑셀 파일, PDF 문서 등에서 데이터를 가져오면 사람이 보기 편한 형식으로 되어 있습니다.

가격에 천 단위 구분 쉼표가 있고, 통화 단위가 붙어 있고, 퍼센트 기호가 붙어 있습니다. 이런 데이터로는 평균을 구하거나 비교할 수 없습니다.

컴퓨터가 이해할 수 있는 순수한 숫자 형태로 바꿔야 합니다. 판다스의 str 접근자가 이 문제를 해결합니다.

판다스 시리즈에서 .str을 사용하면 파이썬의 문자열 메서드를 시리즈 전체에 적용할 수 있습니다. **str.replace()**로 특정 문자를 제거하거나 다른 문자로 바꿀 수 있습니다.

여러 번 연속으로 사용할 수도 있습니다. 콤마를 제거하고, 그 다음 "원"을 제거하는 식입니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 실제 웹사이트에서 가져온 것 같은 데이터를 만듭니다.

가격에는 콤마와 "원"이, 할인율에는 "%"가, 재고에는 "개"가 붙어 있습니다. 가격을 처리할 때는 먼저 **.str.replace(',', '')**로 콤마를 제거하고, 다시 **.str.replace('원', '')**로 "원"을 제거합니다.

마지막으로 **astype(int)**로 정수로 변환합니다. 실제 현업에서는 어떻게 활용할까요?

부동산 매물 정보를 크롤링한다고 가정해봅시다. 매매가는 "3억 5천만원", 월세는 "50/100만원" 같은 형식입니다.

이런 데이터를 분석하려면 먼저 "억", "천만원", "/" 같은 문자를 처리해야 합니다. 더 복잡한 경우에는 정규표현식을 사용할 수도 있습니다.

많은 데이터 분석가들이 매일 이런 작업을 합니다. 박클린 씨가 추가로 설명했습니다.

"문자열 메서드는 매우 강력해요. replace 말고도 다양한 메서드가 있습니다." **str.strip()**은 앞뒤 공백을 제거합니다.

데이터 입력 실수로 " 100 " 같은 값이 있을 때 유용합니다. **str.lower()**와 **str.upper()**는 대소문자를 통일합니다.

국가 코드가 "kr", "KR", "Kr" 등으로 섞여 있을 때 통일할 수 있습니다. **str.split()**은 문자열을 나눕니다.

"홍길동/30세"를 이름과 나이로 분리할 수 있습니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 모든 문자열 처리를 반복문으로 하는 것입니다. 데이터프레임의 각 행을 for 루프로 돌면서 처리하면 매우 느립니다.

판다스의 벡터화된 연산인 .str 메서드를 사용하면 훨씬 빠릅니다. 또한 replace할 때 정규표현식을 사용하려면 regex=True 옵션을 설정해야 합니다.

다시 김데이터 씨의 이야기로 돌아가 봅시다. 김데이터 씨는 제품 데이터의 모든 문자열 컬럼을 점검했습니다.

가격, 할인율, 재고뿐만 아니라 배송비, 중량 등 다양한 컬럼에서 단위와 기호를 제거했습니다. "이제 제품별 할인가를 계산할 수 있어요!" 김데이터 씨가 자신감을 얻었습니다.

문자열 전처리를 제대로 이해하면 어떤 형식의 데이터든 분석 가능한 형태로 변환할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - **str.extract()**로 정규표현식을 사용해 숫자만 추출할 수 있습니다

  • 여러 문자를 한 번에 제거하려면 정규표현식 **[,원%개]**를 사용하세요
  • 변환 전에 **value_counts()**로 어떤 형식의 값들이 있는지 확인하세요

6. 범주형 데이터 처리

김데이터 씨가 고객의 선호 카테고리를 분석하려고 했습니다. 그런데 같은 카테고리가 "전자제품", "전자 제품", "Electronics" 등 여러 형태로 저장되어 있었습니다.

"이걸 어떻게 통일하죠?" 박클린 씨가 답했습니다. "범주형 데이터는 표준화가 중요해요."

범주형 데이터는 제한된 개수의 고유한 값을 가지는 데이터입니다. 마치 설문지의 객관식 선택지처럼 정해진 범주 중에서 선택하는 값입니다.

이런 데이터는 표준화하고 효율적으로 저장해야 분석이 용이합니다.

다음 코드를 살펴봅시다.

import pandas as pd

# 입력 오류가 있는 카테고리 데이터
orders = pd.DataFrame({
    'order_id': [1, 2, 3, 4, 5, 6],
    'category': ['전자제품', '전자 제품', 'Electronics', '의류', '의류 ', '가전'],
    'payment_method': ['카드', '카드', 'card', '현금', 'CARD', '계좌이체']
})

print("정제 전:\n", orders)

# 카테고리 표준화: 공백 제거 및 통일
category_mapping = {
    '전자제품': '전자제품',
    '전자 제품': '전자제품',
    'Electronics': '전자제품',
    '의류': '의류',
    '의류 ': '의류',
    '가전': '가전제품'
}

orders['category'] = orders['category'].map(category_mapping)

# 결제 방법 표준화: 소문자로 통일 후 한글로 매핑
orders['payment_method'] = orders['payment_method'].str.lower()
payment_mapping = {'카드': '카드', 'card': '카드', '현금': '현금', '계좌이체': '계좌이체'}
orders['payment_method'] = orders['payment_method'].map(payment_mapping)

# 범주형 타입으로 변환 (메모리 절약)
orders['category'] = orders['category'].astype('category')
orders['payment_method'] = orders['payment_method'].astype('category')

print("\n정제 후:\n", orders)
print("\n카테고리별 주문 수:\n", orders['category'].value_counts())

김데이터 씨는 새로운 문제에 직면했습니다. 고객이 선택한 카테고리를 집계하려는데, 같은 의미의 값이 여러 형태로 저장되어 있었습니다.

이대로 집계하면 "전자제품"과 "전자 제품"이 별개로 카운트됩니다. 박클린 씨가 설명했습니다.

"범주형 데이터는 실수로 입력되는 경우가 많아요. 사용자가 직접 타이핑하거나, 여러 시스템에서 데이터가 합쳐질 때 이런 일이 생깁니다." 범주형 데이터란 정확히 무엇일까요?

쉽게 비유하자면, 범주형 데이터는 마치 옷 사이즈와 같습니다. S, M, L, XL처럼 정해진 선택지가 있습니다.

숫자처럼 연속적인 값이 아니라, 몇 가지 카테고리 중 하나를 선택하는 것입니다. 성별, 지역, 등급, 색상 등이 모두 범주형 데이터입니다.

범주형 데이터의 문제는 무엇일까요? 첫째, 입력 오류가 많습니다.

사용자가 "서울", "서울시", "Seoul" 등으로 다르게 입력할 수 있습니다. 둘째, 메모리 낭비가 심합니다.

"전자제품"이라는 문자열을 1000번 저장하면 같은 내용을 1000번 반복해서 메모리에 담는 것입니다. 셋째, 집계와 분석이 어렵습니다.

같은 카테고리가 여러 이름으로 흩어져 있으면 정확한 통계를 낼 수 없습니다. 바로 이런 문제를 해결하기 위해 범주형 데이터 처리가 필요합니다.

판다스의 map() 메서드를 사용하면 값을 표준화된 형태로 변환할 수 있습니다. 매핑 딕셔너리를 만들어서 "전자 제품" → "전자제품", "Electronics" → "전자제품"처럼 통일된 값으로 바꿉니다.

또한 category 타입으로 변환하면 내부적으로 정수 코드로 저장되어 메모리를 크게 절약할 수 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 실제 운영 데이터처럼 입력 오류가 있는 주문 데이터를 만듭니다. 같은 카테고리가 여러 형태로 저장되어 있습니다.

category_mapping 딕셔너리를 만들어서 모든 변형을 표준 값으로 매핑합니다. map() 메서드로 이 딕셔너리를 적용하면 모든 값이 통일됩니다.

마지막으로 **astype('category')**로 범주형 타입으로 변환합니다. 실제 현업에서는 어떻게 활용할까요?

고객 리뷰 데이터를 분석한다고 가정해봅시다. 평점이 "5점", "5.0", "★★★★★", "매우만족" 등 다양한 형식으로 입력되어 있습니다.

이걸 모두 1~5의 숫자로 표준화해야 평균 평점을 계산할 수 있습니다. 많은 이커머스 기업들이 이런 방식으로 고객 피드백을 정제합니다.

박클린 씨가 추가 팁을 알려주었습니다. "범주형 데이터를 다룰 때는 항상 **unique()**나 **value_counts()**로 어떤 값들이 있는지 먼저 확인하세요." 실제로 김데이터 씨가 확인해보니 예상치 못한 값들이 많았습니다.

오타, 공백, 대소문자 차이 등이 섞여 있었습니다. 이런 값들을 모두 찾아서 매핑 딕셔너리에 추가했습니다.

또한 매핑되지 않은 값이 있는지 확인하기 위해 변환 후 다시 **value_counts()**를 실행했습니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 매핑 딕셔너리를 수동으로 하드코딩하는 것입니다. 카테고리가 수백 개라면 일일이 타이핑하기 어렵습니다.

이런 경우 **str.strip()**으로 공백을 제거하고, **str.lower()**로 소문자로 통일한 후 매핑하면 더 효율적입니다. 또한 매핑되지 않는 값이 있으면 NaN이 되므로, 변환 후 결측치를 확인해야 합니다.

다시 김데이터 씨의 이야기로 돌아가 봅시다. 김데이터 씨는 모든 범주형 컬럼을 점검하고 표준화했습니다.

카테고리뿐만 아니라 배송 방법, 회원 등급, 지역 등도 통일된 형식으로 정제했습니다. "카테고리별 매출 분석이 정확해졌어요!" 김데이터 씨가 뿌듯해했습니다.

범주형 데이터 처리를 제대로 이해하면 데이터의 일관성을 유지하고 효율적인 분석을 할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - **get_dummies()**로 범주형 데이터를 원-핫 인코딩할 수 있습니다

  • category 타입은 순서가 있는 경우 ordered=True로 설정하세요
  • 새로운 카테고리가 추가될 수 있다면 매핑 딕셔너리를 별도 파일로 관리하세요

7. 정제 전후 비교하기

김데이터 씨가 모든 데이터 정제를 마쳤습니다. "이제 분석을 시작하면 될까요?" 박클린 씨가 고개를 저었습니다.

"잠깐, 정제가 제대로 됐는지 검증부터 해야죠. 전후를 비교해보세요."

정제 전후 비교는 데이터 정제 작업이 올바르게 수행되었는지 확인하는 필수 단계입니다. 마치 요리를 하고 나서 맛을 보는 것처럼, 정제한 데이터의 품질을 검증해야 합니다.

이 과정을 통해 실수를 발견하고 데이터의 신뢰성을 확보합니다.

다음 코드를 살펴봅시다.

import pandas as pd
import numpy as np

# 정제 전 원본 데이터
raw_data = pd.DataFrame({
    'customer_id': [1, 2, 3, 4, 5],
    'age': ['25', np.nan, '35', '30', 'unknown'],
    'purchase_amount': ['50,000원', '120,000원', np.nan, '80,000원', '200,000원'],
    'category': ['전자제품', '전자 제품', '의류', 'Electronics', '가전']
})

print("=" * 50)
print("정제 전 데이터 상태")
print("=" * 50)
print("\n기본 정보:")
print(raw_data.info())
print("\n데이터 샘플:")
print(raw_data)
print("\n결측치 개수:")
print(raw_data.isna().sum())

# 데이터 정제 수행
clean_data = raw_data.copy()

# 1. 나이: 'unknown'을 NaN으로, 결측치는 평균으로 채움
clean_data['age'] = clean_data['age'].replace('unknown', np.nan)
clean_data['age'] = pd.to_numeric(clean_data['age'], errors='coerce')
clean_data['age'].fillna(clean_data['age'].mean(), inplace=True)

# 2. 구매액: 문자열 정제 후 숫자로 변환
clean_data['purchase_amount'] = clean_data['purchase_amount'].str.replace(',', '').str.replace('원', '')
clean_data['purchase_amount'] = pd.to_numeric(clean_data['purchase_amount'], errors='coerce')
clean_data['purchase_amount'].fillna(clean_data['purchase_amount'].median(), inplace=True)

# 3. 카테고리: 표준화
category_map = {'전자제품': '전자제품', '전자 제품': '전자제품', 'Electronics': '전자제품', '의류': '의류', '가전': '가전제품'}
clean_data['category'] = clean_data['category'].map(category_map)

print("\n" + "=" * 50)
print("정제 후 데이터 상태")
print("=" * 50)
print("\n기본 정보:")
print(clean_data.info())
print("\n데이터 샘플:")
print(clean_data)
print("\n결측치 개수:")
print(clean_data.isna().sum())
print("\n통계 요약:")
print(clean_data.describe())

김데이터 씨는 모든 정제 작업을 마치고 뿌듯했습니다. 하지만 박클린 씨는 아직 끝나지 않았다고 했습니다.

"정제를 했다고 끝이 아니에요. 제대로 됐는지 확인해야죠." "어떻게 확인하나요?" 김데이터 씨가 물었습니다.

박클린 씨가 답했습니다. "정제 전 데이터와 정제 후 데이터를 비교하는 거예요.

무엇이 바뀌었는지, 제대로 바뀌었는지 확인하는 겁니다." 정제 전후 비교란 정확히 무엇일까요? 쉽게 비유하자면, 정제 전후 비교는 마치 건강검진 결과를 전년도와 비교하는 것과 같습니다.

몸무게가 줄었는지, 혈압이 정상 범위인지 확인합니다. 데이터도 마찬가지입니다.

결측치가 줄었는지, 타입이 올바른지, 값이 정상 범위인지 검증해야 합니다. 왜 이런 검증이 필요할까요?

데이터 정제는 복잡한 작업입니다. 실수로 잘못된 값으로 채웠을 수도 있고, 타입 변환 중에 데이터가 손실됐을 수도 있습니다.

예를 들어 나이를 평균으로 채웠는데 150살이 나왔다면 뭔가 잘못된 것입니다. 구매액의 단위를 잘못 해석해서 1000배 차이가 날 수도 있습니다.

이런 오류를 조기에 발견해야 합니다. 판다스의 검증 메서드들이 이를 도와줍니다.

**info()**는 각 컬럼의 타입, non-null 개수, 메모리 사용량을 보여줍니다. 정제 전에는 모두 object 타입이었는데 정제 후에는 int64, float64로 바뀌었는지 확인할 수 있습니다.

**describe()**는 숫자 컬럼의 통계 요약을 보여줍니다. 평균, 최소값, 최대값이 합리적인 범위인지 확인할 수 있습니다.

**value_counts()**는 범주형 데이터의 분포를 보여줍니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 정제 전 데이터의 상태를 출력합니다. **info()**로 타입과 결측치를 확인하고, **isna().sum()**으로 컬럼별 결측치 개수를 확인합니다.

다음으로 데이터 정제를 수행합니다. 나이, 구매액, 카테고리를 각각 처리합니다.

마지막으로 정제 후 데이터의 상태를 다시 출력하고, **describe()**로 통계 요약까지 확인합니다. 실제 현업에서는 어떻게 활용할까요?

금융 데이터를 정제한다고 가정해봅시다. 대출 금액을 정제한 후 최대값을 확인했더니 100조 원이 나왔습니다.

명백한 오류입니다. 아마 단위 변환 중에 실수가 있었을 것입니다.

이처럼 describe()의 max 값을 보면 비정상적인 값을 쉽게 발견할 수 있습니다. 많은 데이터 엔지니어들이 정제 후 반드시 이런 검증을 수행합니다.

박클린 씨가 체크리스트를 알려주었습니다. "정제 후에는 항상 이것들을 확인하세요." 첫째, 결측치가 의도한 대로 처리되었는지 확인합니다.

**isna().sum()**이 0이 되었거나, 허용 가능한 수준으로 줄었는지 봅니다. 둘째, 데이터 타입이 올바른지 확인합니다.

숫자는 int나 float, 범주는 category 타입인지 봅니다. 셋째, 값의 범위가 합리적인지 확인합니다.

나이가 0~120, 가격이 양수인지 등을 봅니다. 넷째, 범주형 데이터가 의도한 카테고리로 통일되었는지 **value_counts()**로 확인합니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 검증 없이 바로 분석을 시작하는 것입니다.

잘못 정제된 데이터로 분석하면 결론이 완전히 틀릴 수 있습니다. "쓰레기를 넣으면 쓰레기가 나온다"는 말처럼 데이터 품질이 분석 품질을 결정합니다.

따라서 정제 후 검증은 선택이 아니라 필수입니다. 또한 정제 전 원본 데이터는 반드시 백업해두어야 합니다.

다시 김데이터 씨의 이야기로 돌아가 봅시다. 김데이터 씨는 정제 전후를 꼼꼼히 비교했습니다.

결측치가 80%에서 0%로 줄었고, 모든 타입이 올바르게 변환되었으며, 통계 요약도 합리적인 범위였습니다. "이제 자신 있게 분석 결과를 보고할 수 있겠어요!" 김데이터 씨가 만족스러워했습니다.

박클린 씨가 마지막으로 조언했습니다. "데이터 정제는 분석의 80%를 차지합니다.

지루하지만 가장 중요한 작업이에요. 오늘 배운 기법들을 실무에서 꼭 활용하세요." 정제 전후 비교를 제대로 이해하면 데이터 품질을 보장하고 신뢰할 수 있는 분석을 할 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - assert 문으로 검증을 자동화할 수 있습니다 (예: assert df['age'].min() >= 0)

  • 정제 전 데이터는 **df_raw.copy()**로 백업해두세요
  • 시각화(히스토그램, 박스플롯)로 분포 변화를 확인하면 더 명확합니다

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Python#Pandas#DataCleaning#DataProcessing#NullHandling

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.