🤖

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

⚠️

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

이미지 로딩 중...

데이터 품질 확인 완벽 가이드 - 슬라이드 1/8
A

AI Generated

2025. 12. 16. · 8 Views

데이터 품질 확인 완벽 가이드

결측치, 중복, 이상치를 찾아내는 데이터 품질 확인 방법을 배웁니다. 초급 개발자도 쉽게 따라할 수 있는 실무 중심 가이드입니다.


목차

  1. 결측치란_무엇인가
  2. isnull로_결측치_확인하기
  3. 결측치_비율_계산하기
  4. duplicated로_중복_확인하기
  5. 이상치_탐지_방법
  6. 박스플롯으로_이상치_시각화
  7. 데이터_품질_리포트_작성

1. 결측치란 무엇인가

어느 날 김데이터 씨가 고객 데이터를 분석하던 중 이상한 현상을 발견했습니다. 분명히 1000명의 고객 데이터가 있는데, 나이 평균을 계산하니 엉뚱한 값이 나왔습니다.

선배 박분석 씨가 다가와 물었습니다. "혹시 결측치 확인해봤어요?"

결측치는 데이터셋에서 값이 비어있는 부분을 말합니다. 마치 설문조사에서 누군가 답변을 건너뛴 것과 같습니다.

결측치를 제대로 처리하지 않으면 분석 결과가 왜곡될 수 있습니다. 데이터 품질 확인의 첫 번째 단계는 바로 결측치를 찾아내는 것입니다.

다음 코드를 살펴봅시다.

import pandas as pd
import numpy as np

# 샘플 데이터 생성 (일부 결측치 포함)
data = {
    'name': ['김철수', '이영희', '박민수', None, '최지원'],
    'age': [25, None, 35, 28, 32],
    'city': ['서울', '부산', None, '대구', '인천']
}
df = pd.DataFrame(data)

# 결측치 확인
print("결측치 개수:")
print(df.isnull().sum())

김데이터 씨는 입사 2개월 차 데이터 분석가입니다. 오늘은 마케팅 팀에서 요청한 고객 분석 리포트를 작성하고 있습니다.

엑셀 파일을 받아서 열심히 분석하던 중, 이상한 점을 발견했습니다. 고객 나이의 평균을 계산했더니 35세가 나왔는데, 눈으로 보기에는 20대 고객도 꽤 많아 보였습니다.

"뭔가 이상한데?" 김데이터 씨는 고개를 갸우뚱했습니다. 선배 박분석 씨가 자리에서 일어나 김데이터 씨의 모니터를 살펴봅니다.

"아, 결측치 확인 안 했구나. 데이터 분석의 첫 단계인데 말이야." 결측치란 정확히 무엇일까요?

쉽게 비유하자면, 결측치는 마치 시험지에서 학생이 답을 쓰지 않고 빈칸으로 남겨둔 것과 같습니다. 100명의 학생이 시험을 봤는데 10명이 5번 문제를 안 풀었다면, 5번 문제의 평균 점수를 계산할 때 90명의 답만 사용해야 합니다.

만약 빈칸을 0점으로 처리하거나 무시하고 계산하면 평균이 왜곡됩니다. 데이터 분석에서도 마찬가지입니다.

결측치는 None, NaN(Not a Number), NULL 등 여러 형태로 나타납니다. Pandas 라이브러리에서는 이들을 모두 인식하고 처리할 수 있습니다.

왜 결측치가 생길까요? 실무에서 결측치가 발생하는 이유는 다양합니다.

고객이 회원가입할 때 선택 항목을 입력하지 않았거나, 센서가 일시적으로 오작동해서 데이터를 수집하지 못했거나, 서로 다른 시스템을 통합하는 과정에서 매칭되지 않는 데이터가 생길 수 있습니다. 김데이터 씨의 경우, 마케팅 팀에서 받은 엑셀 파일에는 일부 고객의 나이 정보가 비어있었습니다.

이런 결측치를 그냥 무시하고 평균을 계산하면 실제보다 높거나 낮은 값이 나올 수 있습니다. 결측치를 확인하는 방법은 간단합니다.

위의 코드를 살펴보겠습니다. 먼저 샘플 데이터를 만들었습니다.

name 컬럼의 네 번째 값과 age 컬럼의 두 번째 값, city 컬럼의 세 번째 값을 None으로 설정했습니다. 이것이 바로 결측치입니다.

isnull() 메서드를 사용하면 각 셀이 결측치인지 확인할 수 있습니다. 결측치면 True, 값이 있으면 False를 반환합니다.

여기에 **sum()**을 붙이면 각 컬럼별로 결측치가 몇 개인지 계산됩니다. 출력 결과를 보면 name 컬럼에 1개, age 컬럼에 1개, city 컬럼에 1개의 결측치가 있다는 것을 한눈에 알 수 있습니다.

실제 프로젝트에서는 어떻게 활용할까요? 예를 들어 전자상거래 회사에서 고객 데이터를 분석한다고 가정해봅시다.

10만 명의 고객 데이터가 있고, 그중 2만 명의 전화번호가 비어있다면 어떻게 해야 할까요? 전화번호가 필수인 SMS 마케팅 캠페인을 기획한다면, 전화번호가 있는 8만 명만 대상으로 삼아야 합니다.

또는 머신러닝 모델을 학습시킬 때도 결측치 확인은 필수입니다. 대부분의 알고리즘은 결측치가 있으면 에러를 발생시키거나 제대로 학습하지 못합니다.

주의할 점도 있습니다. 초보 분석가들이 흔히 하는 실수는 결측치를 무조건 삭제하는 것입니다.

만약 데이터의 50%가 결측치라면 모두 삭제했을 때 남는 데이터가 너무 적어집니다. 이럴 때는 평균값으로 채우거나, 다른 컬럼의 값을 참고해서 추정하거나, 결측치가 있어도 분석 가능한 방법을 찾아야 합니다.

박분석 씨의 설명을 들은 김데이터 씨는 고개를 끄덕였습니다. "아, 그래서 평균이 이상하게 나왔군요!" 즉시 결측치를 확인하고 적절히 처리한 후, 다시 분석을 시작했습니다.

이번에는 정확한 결과가 나왔습니다. 결측치 확인은 데이터 분석의 기초입니다.

어떤 분석을 하든 가장 먼저 해야 할 일이 바로 결측치를 찾아내고 이해하는 것입니다.

실전 팁

💡 - 데이터를 받으면 가장 먼저 **df.info()**로 전체 현황을 파악하세요

  • 결측치 비율이 70%를 넘으면 해당 컬럼의 유용성을 재검토하세요
  • 시계열 데이터의 결측치는 앞뒤 값으로 보간하는 방법도 고려하세요

2. isnull로 결측치 확인하기

김데이터 씨가 결측치의 개념을 이해한 후, 박분석 씨가 물었습니다. "그럼 실제로 어떤 행에 결측치가 있는지 찾아볼 수 있겠어요?" 김데이터 씨는 잠시 생각에 잠겼습니다.

개수는 알겠는데, 정확히 어느 위치에 있는지는 어떻게 찾을까요?

isnull() 메서드는 데이터프레임의 각 셀이 결측치인지 확인하는 기본 도구입니다. 마치 문서에서 형광펜으로 빈칸을 표시하는 것과 같습니다.

**notnull()**과 함께 사용하면 결측치가 있는 행만 필터링할 수도 있습니다. 결측치를 시각적으로 파악하는 첫 번째 단계입니다.

다음 코드를 살펴봅시다.

import pandas as pd
import numpy as np

# 고객 데이터 생성
customers = {
    'id': [1, 2, 3, 4, 5],
    'name': ['김철수', None, '박민수', '이영희', '최지원'],
    'email': ['kim@email.com', 'lee@email.com', None, 'park@email.com', None],
    'age': [25, 30, None, 28, 35]
}
df = pd.DataFrame(customers)

# 결측치를 True/False로 표시
print(df.isnull())

# 결측치가 있는 행만 필터링
rows_with_null = df[df.isnull().any(axis=1)]
print("\n결측치가 있는 행:")
print(rows_with_null)

박분석 씨가 새로운 과제를 제시했습니다. "개수만 아는 것으로는 부족해요.

실제로 어떤 고객의 데이터가 빠져있는지 확인해야 후속 조치를 할 수 있죠." 김데이터 씨는 고개를 끄덕였습니다. 확실히 그렇습니다.

1000명 중 100명의 이메일이 없다는 것만 알아서는 안 됩니다. 정확히 어떤 고객의 이메일이 없는지 알아야 그 고객들에게 이메일을 다시 요청하거나 다른 방법으로 연락할 수 있습니다.

isnull() 메서드의 진정한 힘은 바로 여기에 있습니다. **isnull()**을 단독으로 사용하면 데이터프레임과 같은 크기의 불리언(Boolean) 데이터프레임을 반환합니다.

각 셀이 결측치면 True, 값이 있으면 False입니다. 이것은 마치 투명 필름에 빈칸만 표시한 것과 같습니다.

위의 코드를 실행하면 어떻게 될까요? 첫 번째 **print(df.isnull())**을 실행하면 5행 4열의 불리언 데이터프레임이 출력됩니다.

id 컬럼은 모두 False입니다. 값이 다 있으니까요.

name 컬럼의 두 번째 행은 True입니다. 여기가 결측치니까요.

하지만 단순히 True/False를 보는 것보다 더 유용한 방법이 있습니다. 바로 결측치가 있는 행만 골라내는 것입니다.

df[df.isnull().any(axis=1)] 이 코드를 살펴보겠습니다. 안쪽부터 읽어봅시다.

**df.isnull()**은 앞에서 본 불리언 데이터프레임을 만듭니다. 여기에 **any(axis=1)**을 붙이면 각 행에 True가 하나라도 있는지 확인합니다.

axis=1은 행 방향으로 확인한다는 뜻입니다. axis=0이면 열 방향, axis=1이면 행 방향입니다.

이것은 Pandas에서 자주 사용되는 개념이니 꼭 기억해두세요. 결과적으로 **any(axis=1)**은 각 행마다 하나의 True/False 값을 반환합니다.

그 행에 결측치가 하나라도 있으면 True, 모두 값이 있으면 False입니다. 이것을 대괄호 안에 넣으면 True인 행만 선택됩니다.

실무에서는 어떻게 활용할까요? 온라인 쇼핑몰에서 회원가입 데이터를 분석한다고 가정해봅시다.

필수 입력 항목은 모두 채워져야 하는데, 시스템 오류로 일부 데이터가 누락되었을 수 있습니다. **isnull()**을 사용하면 문제가 있는 회원 데이터를 빠르게 찾아낼 수 있습니다.

또는 설문조사 데이터를 분석할 때, 특정 질문에 답하지 않은 응답자를 찾아내서 재질문하거나 분석에서 제외할 수 있습니다. notnull() 메서드도 알아두면 좋습니다.

**notnull()**은 **isnull()**의 반대입니다. 값이 있으면 True, 결측치면 False를 반환합니다.

만약 이메일이 있는 고객만 추출하고 싶다면 **df[df['email'].notnull()]**처럼 사용할 수 있습니다. 초보자들이 자주 하는 실수가 있습니다.

**isnull()**과 **isna()**를 헷갈려 하는데, 사실 이 둘은 완전히 같은 기능입니다. Pandas에서는 두 이름을 모두 지원합니다.

마찬가지로 **notnull()**과 **notna()**도 같습니다. 어떤 것을 사용해도 무방하지만, 팀에서 정한 컨벤션을 따르는 것이 좋습니다.

김데이터 씨는 코드를 실행해봤습니다. 결측치가 있는 행들이 깔끔하게 필터링되어 출력되었습니다.

"오! 이제 어떤 고객의 정보가 빠졌는지 정확히 알 수 있네요!" 박분석 씨가 미소를 지으며 말했습니다.

"맞아요. 데이터 분석은 이렇게 하나씩 확인하면서 진행하는 거예요."

실전 팁

💡 - **df.isnull().sum()**은 컬럼별 결측치 개수를 빠르게 확인할 때 유용합니다

  • **df.isnull().sum().sum()**은 전체 결측치 개수를 한 번에 계산합니다
  • 특정 컬럼만 확인하려면 **df['column_name'].isnull()**을 사용하세요

3. 결측치 비율 계산하기

결측치의 위치를 파악한 김데이터 씨에게 박분석 씨가 다음 질문을 던졌습니다. "그럼 이제 각 컬럼의 결측치 비율을 계산해볼까요?

비율을 보면 어떤 컬럼이 심각한지 한눈에 알 수 있거든요." 김데이터 씨는 계산기를 꺼내려다가 멈췄습니다. 분명 더 스마트한 방법이 있을 것 같습니다.

결측치 비율은 전체 데이터 중 몇 퍼센트가 비어있는지 나타냅니다. 마치 출석부에서 결석률을 계산하는 것과 같습니다.

비율을 계산하면 어떤 컬럼을 우선적으로 처리해야 할지 판단할 수 있습니다. 일반적으로 결측치 비율이 30%를 넘으면 해당 컬럼의 신뢰성을 재검토해야 합니다.

다음 코드를 살펴봅시다.

import pandas as pd
import numpy as np

# 큰 데이터셋 시뮬레이션
np.random.seed(42)
data = {
    'user_id': range(1, 101),
    'name': ['User' + str(i) if i % 10 != 0 else None for i in range(1, 101)],
    'phone': ['010-' + str(i) if i % 5 != 0 else None for i in range(1, 101)],
    'address': ['Address' + str(i) if i % 3 != 0 else None for i in range(1, 101)]
}
df = pd.DataFrame(data)

# 결측치 비율 계산 (백분율)
missing_ratio = (df.isnull().sum() / len(df)) * 100
print("컬럼별 결측치 비율(%):")
print(missing_ratio.round(2))

# 결측치 비율이 20% 이상인 컬럼만 필터링
high_missing = missing_ratio[missing_ratio >= 20]
print("\n결측치 비율 20% 이상 컬럼:")
print(high_missing)

김데이터 씨는 이제 결측치가 어디에 있는지 찾을 수 있습니다. 하지만 100개의 컬럼이 있는 데이터셋에서 하나하나 확인하기는 힘듭니다.

더 효율적인 방법이 필요합니다. 박분석 씨가 화이트보드에 간단한 공식을 그렸습니다.

"결측치 비율 = (결측치 개수 / 전체 데이터 개수) × 100. 이렇게 계산하면 되죠." 결측치 비율은 왜 중요할까요?

쉽게 비유하자면, 결측치 비율은 마치 학생의 출석률과 같습니다. 한 학기에 50일 중 5일 결석한 학생의 출석률은 90%입니다.

만약 30일 결석했다면 출석률은 40%로, 이 학생의 성적을 평가하기 어렵습니다. 데이터 분석도 마찬가지입니다.

어떤 컬럼의 결측치 비율이 10%라면 나머지 90% 데이터로 분석을 진행할 수 있습니다. 하지만 결측치 비율이 60%라면 그 컬럼은 거의 쓸모가 없습니다.

실무에서 결측치 비율의 기준은 어떻게 될까요? 경험상 20% 미만이면 양호한 편입니다.

**20~50%**는 주의가 필요하며, 결측치 처리 방법을 신중히 선택해야 합니다. 50% 이상이면 해당 컬럼을 아예 제거하거나 수집 방법을 개선해야 합니다.

위의 코드를 단계별로 살펴보겠습니다. 먼저 100명의 사용자 데이터를 시뮬레이션했습니다.

name 컬럼은 10의 배수 행에서 결측치가 발생합니다. 즉 10%의 결측치 비율입니다.

phone 컬럼은 5의 배수 행에서 결측치가 발생하므로 20% 비율입니다. address 컬럼은 3의 배수 행에서 결측치가 발생하므로 약 33% 비율입니다.

**df.isnull().sum()**은 각 컬럼의 결측치 개수를 계산합니다. 여기에 **len(df)**로 나누면 비율이 나옵니다.

**len(df)**는 데이터프레임의 행 개수, 즉 전체 데이터 개수입니다. 100을 곱하면 백분율로 변환됩니다.

**round(2)**는 소수점 둘째 자리까지만 표시합니다. 34.56789%보다는 34.57%가 훨씬 보기 좋으니까요.

결과를 보면 user_id 컬럼은 0%, name 컬럼은 10%, phone 컬럼은 20%, address 컬럼은 약 33%의 결측치 비율을 보입니다. 추가로 결측치 비율이 20% 이상인 컬럼만 필터링했습니다.

missing_ratio[missing_ratio >= 20] 이 코드는 시리즈에서 조건에 맞는 항목만 선택합니다. 결과적으로 phoneaddress 컬럼만 출력됩니다.

실제 프로젝트에서는 어떻게 활용할까요? 예를 들어 의료 데이터를 분석한다고 가정해봅시다.

환자의 혈압, 혈당, 콜레스테롤 등 여러 지표를 수집했는데, 혈당 데이터의 결측치 비율이 60%라면 어떻게 해야 할까요? 이 경우 혈당 데이터를 기반으로 한 분석은 신뢰하기 어렵습니다.

데이터 수집 프로세스를 개선하거나, 다른 지표로 대체하는 방법을 고려해야 합니다. 또 다른 예시로 웹 로그 분석을 생각해봅시다.

사용자의 IP, 접속 시간, 페이지 URL은 거의 100% 수집되지만, 사용자의 디바이스 정보나 브라우저 정보는 일부 누락될 수 있습니다. 결측치 비율을 계산하면 어떤 정보가 안정적으로 수집되고 있는지 파악할 수 있습니다.

주의할 점도 있습니다. 결측치 비율만 보고 컬럼을 삭제하면 안 됩니다.

비록 비율이 높더라도 중요한 정보일 수 있습니다. 예를 들어 고객의 VIP 등급 정보가 80%나 결측치라고 해서 삭제하면, 나머지 20%의 VIP 고객 분석을 할 수 없게 됩니다.

또한 결측치가 무작위로 발생했는지, 특정 패턴이 있는지도 확인해야 합니다. 만약 특정 지역의 고객만 전화번호가 누락되었다면, 이것은 시스템 오류일 수 있습니다.

김데이터 씨가 코드를 실행해봤습니다. 각 컬럼의 결측치 비율이 깔끔하게 정리되어 출력되었습니다.

"와, 이렇게 보니까 어떤 컬럼에 집중해야 할지 바로 알 수 있네요!" 박분석 씨가 고개를 끄덕였습니다. "맞아요.

데이터 품질 리포트를 작성할 때 가장 먼저 보여줘야 하는 지표가 바로 결측치 비율이에요."

실전 팁

💡 - 결측치 비율 계산을 함수로 만들어두면 재사용하기 편리합니다

  • **df.info()**를 사용하면 각 컬럼의 non-null 개수를 빠르게 확인할 수 있습니다
  • 시각화가 필요하면 matplotlib이나 seaborn으로 막대 그래프를 그려보세요

4. duplicated로 중복 확인하기

결측치 분석을 마친 김데이터 씨가 다음 단계로 넘어가려는데, 박분석 씨가 또 다른 중요한 포인트를 짚어줬습니다. "결측치만큼 중요한 게 중복 데이터예요.

똑같은 고객이 두 번 들어가 있으면 집계가 잘못되거든요." 김데이터 씨는 깜짝 놀랐습니다. 중복까지 확인해야 한다니!

duplicated() 메서드는 데이터프레임에서 중복된 행을 찾아냅니다. 마치 명단에서 같은 이름이 두 번 적힌 것을 찾는 것과 같습니다.

중복 데이터는 집계 결과를 왜곡시키고, 통계 분석의 신뢰성을 떨어뜨립니다. 데이터 정제의 핵심 단계 중 하나입니다.

다음 코드를 살펴봅시다.

import pandas as pd

# 중복이 포함된 주문 데이터
orders = {
    'order_id': [1001, 1002, 1003, 1002, 1004, 1003],
    'customer': ['김철수', '이영희', '박민수', '이영희', '최지원', '박민수'],
    'product': ['노트북', '마우스', '키보드', '마우스', '모니터', '키보드'],
    'amount': [1500000, 30000, 80000, 30000, 400000, 80000]
}
df = pd.DataFrame(orders)

# 중복 행 확인 (기본: 첫 번째 제외한 중복만 True)
print("중복 행 표시:")
print(df.duplicated())

# 중복된 행만 추출
duplicates = df[df.duplicated(keep=False)]
print("\n중복된 모든 행:")
print(duplicates)

# 특정 컬럼 기준으로 중복 확인
dup_by_customer = df[df.duplicated(subset=['customer'], keep=False)]
print("\n고객 기준 중복:")
print(dup_by_customer)

김데이터 씨는 이제까지 결측치만 신경 쓰면 된다고 생각했습니다. 하지만 박분석 씨의 말을 듣고 보니, 중복 데이터도 큰 문제가 될 수 있다는 것을 깨달았습니다.

중복 데이터는 왜 문제일까요? 쉽게 비유하자면, 중복 데이터는 마치 투표함에 같은 사람이 두 번 투표한 것과 같습니다.

100명이 투표했는데 실제로는 10명이 중복 투표해서 총 110표가 나왔다면, 결과가 왜곡됩니다. 진짜 100명의 의견이 아니라 일부는 두 배로 반영된 셈입니다.

데이터 분석에서도 마찬가지입니다. 매출 데이터에서 같은 주문이 두 번 들어가 있으면 총 매출액이 실제보다 높게 계산됩니다.

고객 수를 세는데 같은 고객이 여러 번 들어가 있으면 고객 수가 부풀려집니다. 중복 데이터는 어떻게 발생할까요?

시스템 오류로 같은 데이터가 두 번 저장되거나, 서로 다른 데이터베이스를 병합하는 과정에서 중복이 생기거나, 사용자가 실수로 같은 작업을 두 번 수행할 수 있습니다. 실무에서는 생각보다 자주 발생합니다.

duplicated() 메서드의 기본 동작을 이해해봅시다. **df.duplicated()**를 실행하면 각 행마다 True 또는 False를 반환합니다.

처음 나타난 행은 False, 이후 중복된 행은 True입니다. 위의 코드에서 인덱스 1번 행과 인덱스 3번 행은 완전히 같습니다.

따라서 인덱스 3번은 True가 됩니다. 하지만 keep 매개변수를 조절하면 동작이 달라집니다.

**keep='first'**는 기본값으로, 첫 번째 행은 False, 나머지 중복은 True입니다. **keep='last'**는 반대로 마지막 행은 False, 앞의 중복은 True입니다.

keep=False는 중복된 모든 행을 True로 표시합니다. keep=False가 유용한 이유는, 중복된 데이터를 확인할 때 원본과 사본을 모두 보고 싶기 때문입니다.

만약 **keep='first'**를 사용하면 첫 번째 행은 보이지 않아서 왜 중복인지 비교하기 어렵습니다. 위의 코드에서 **df[df.duplicated(keep=False)]**를 실행하면 완전히 중복된 행들이 모두 출력됩니다.

인덱스 1, 3번 행과 인덱스 2, 5번 행이 나타납니다. 특정 컬럼만 기준으로 중복을 확인할 수도 있습니다.

subset 매개변수를 사용하면 됩니다. **df.duplicated(subset=['customer'])**는 customer 컬럼만 보고 중복을 판단합니다.

같은 고객이 여러 번 주문했는지 확인하고 싶을 때 유용합니다. 실제 프로젝트에서는 어떻게 활용할까요?

예를 들어 회원가입 데이터를 분석한다고 가정해봅시다. 이메일 주소는 유일해야 하는데, 시스템 오류로 같은 이메일이 두 번 등록되었을 수 있습니다.

**df.duplicated(subset=['email'])**을 사용하면 이런 문제를 빠르게 찾아낼 수 있습니다. 또는 센서 데이터를 분석할 때, 타임스탬프가 완전히 같은 데이터가 두 개 있다면 이것은 중복 전송일 가능성이 높습니다.

**df.duplicated(subset=['timestamp', 'sensor_id'])**로 확인할 수 있습니다. 중복을 찾았다면 어떻게 처리할까요?

drop_duplicates() 메서드를 사용하면 중복을 제거할 수 있습니다. **df.drop_duplicates()**는 중복된 행을 삭제하고 첫 번째 행만 남깁니다.

**df.drop_duplicates(subset=['customer'], keep='last')**처럼 특정 컬럼 기준으로 마지막 행만 남길 수도 있습니다. 하지만 무조건 삭제하기 전에 왜 중복이 발생했는지 확인해야 합니다.

정말 오류인지, 아니면 합법적으로 중복될 수 있는 데이터인지 판단해야 합니다. 김데이터 씨가 자신의 고객 데이터에 **duplicated()**를 적용해봤습니다.

놀랍게도 1000명 중 50명이 중복으로 들어가 있었습니다. "아!

이래서 고객 수가 이상하게 많았구나!" 박분석 씨가 웃으며 말했습니다. "데이터 정제는 이렇게 작은 부분까지 꼼꼼히 확인하는 거예요.

중복 하나 때문에 전체 분석이 틀어질 수 있으니까요."

실전 팁

💡 - 전체 중복뿐 아니라 특정 컬럼 조합의 중복도 확인하세요

  • **df.drop_duplicates(inplace=True)**는 원본 데이터프레임을 직접 수정합니다
  • 중복을 제거하기 전에 반드시 **df.duplicated().sum()**으로 개수를 먼저 확인하세요

5. 이상치 탐지 방법

중복 데이터까지 처리한 김데이터 씨가 분석을 계속하려는데, 매출 통계에서 이상한 값을 발견했습니다. 평균 주문 금액이 50만 원인데, 한 건의 주문이 5억 원입니다.

박분석 씨가 모니터를 보더니 말했습니다. "전형적인 이상치네요.

이것도 확인해야 해요."

이상치는 다른 데이터와 현저히 다른 극단적인 값을 말합니다. 마치 키 170cm인 사람들 사이에 250cm인 사람이 있는 것과 같습니다.

이상치는 데이터 입력 오류일 수도 있고, 실제로 특이한 케이스일 수도 있습니다. IQR(사분위수 범위) 방법과 Z-score 방법이 대표적인 탐지 방법입니다.

다음 코드를 살펴봅시다.

import pandas as pd
import numpy as np

# 주문 금액 데이터 (일부 이상치 포함)
np.random.seed(42)
normal_data = np.random.normal(500000, 100000, 95)  # 평균 50만원, 표준편차 10만원
outliers = [5000000, 8000000, 100000, 12000000, 50000]  # 이상치 추가
amounts = np.concatenate([normal_data, outliers])
df = pd.DataFrame({'order_amount': amounts})

# IQR 방법으로 이상치 탐지
Q1 = df['order_amount'].quantile(0.25)
Q3 = df['order_amount'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

# 이상치 추출
outliers_iqr = df[(df['order_amount'] < lower_bound) | (df['order_amount'] > upper_bound)]
print(f"IQR 방법 이상치 개수: {len(outliers_iqr)}")
print(f"정상 범위: {lower_bound:.0f} ~ {upper_bound:.0f}")

김데이터 씨는 처음에 5억 원짜리 주문을 보고 깜짝 놀랐습니다. "우리 회사 제품 중에 이렇게 비싼 게 있었나?" 하지만 자세히 보니 소수점을 잘못 찍은 것 같습니다.

50만 원이어야 할 것이 5억 원으로 입력되었습니다. 이상치란 정확히 무엇일까요?

쉽게 비유하자면, 이상치는 마치 학급 평균 키가 165cm인데 한 학생만 220cm인 것과 같습니다. 통계적으로 나머지 데이터와 동떨어진 값입니다.

이런 값은 평균이나 합계를 계산할 때 결과를 크게 왜곡시킵니다. 이상치가 생기는 이유는 다양합니다.

첫째, 데이터 입력 오류입니다. 사람이 직접 입력할 때 숫자를 잘못 쓰거나 소수점을 잘못 찍을 수 있습니다.

둘째, 측정 장비의 오작동입니다. 센서가 일시적으로 고장 나서 이상한 값을 보낼 수 있습니다.

셋째, 실제로 특이한 케이스입니다. 대부분 고객은 10만 원어치를 주문하는데, VIP 고객이 1000만 원어치를 주문할 수도 있습니다.

이상치를 탐지하는 가장 일반적인 방법은 IQR(Interquartile Range, 사분위수 범위) 방법입니다. 먼저 사분위수를 이해해야 합니다.

데이터를 작은 것부터 크기 순으로 나열했을 때, **Q1(제1사분위수)**은 하위 25% 지점의 값이고, **Q3(제3사분위수)**은 하위 75% 지점의 값입니다. IQRQ3 - Q1입니다.

즉, 중간 50% 데이터의 범위입니다. 통계학에서는 Q1 - 1.5 × IQR보다 작거나 Q3 + 1.5 × IQR보다 큰 값을 이상치로 봅니다.

이 기준은 대부분의 데이터 분석에서 널리 사용됩니다. 위의 코드를 단계별로 살펴보겠습니다.

먼저 정규분포를 따르는 95개의 정상 데이터를 만들었습니다. 평균은 50만 원, 표준편차는 10만 원입니다.

그리고 의도적으로 5개의 극단적인 값을 추가했습니다. 5백만 원, 8백만 원, 10만 원, 1200만 원, 5만 원입니다.

**quantile(0.25)**는 Q1을 계산하고, **quantile(0.75)**는 Q3를 계산합니다. IQR = Q3 - Q1로 사분위수 범위를 구합니다.

하한선은 Q1 - 1.5 × IQR, 상한선은 Q3 + 1.5 × IQR입니다. 이 범위를 벗어나는 값은 이상치로 간주됩니다.

마지막으로 **df[(df['order_amount'] < lower_bound) | (df['order_amount'] > upper_bound)]**로 이상치를 필터링합니다. **|**는 OR 연산자로, 하한선보다 작거나 상한선보다 큰 값을 선택합니다.

결과를 보면 5개 정도의 이상치가 탐지됩니다. 실제로 우리가 추가한 극단적인 값들입니다.

또 다른 방법으로 Z-score 방법도 있습니다. Z-score는 어떤 값이 평균에서 표준편차 몇 개만큼 떨어져 있는지 나타냅니다.

Z-score = (값 - 평균) / 표준편차로 계산합니다. 일반적으로 Z-score의 절댓값이 3보다 크면 이상치로 봅니다.

하지만 Z-score 방법은 데이터가 정규분포를 따른다고 가정합니다. 실제 데이터는 항상 정규분포를 따르지 않으므로, IQR 방법이 더 안전합니다.

실제 프로젝트에서는 어떻게 활용할까요? 예를 들어 웹사이트의 페이지 로딩 시간을 분석한다고 가정해봅시다.

대부분 2~3초인데, 어떤 요청은 300초가 걸렸습니다. 이것은 네트워크 타임아웃이나 서버 오류일 가능성이 높습니다.

이상치를 찾아내면 시스템 문제를 진단할 수 있습니다. 또는 제조업에서 제품의 무게를 측정할 때, 대부분 100g인데 한 제품이 200g이라면 불량품일 수 있습니다.

이상치 탐지는 품질 관리에도 유용합니다. 이상치를 찾았다면 어떻게 처리할까요?

먼저 확인해야 합니다. 정말 오류인지, 아니면 의미 있는 데이터인지 판단합니다.

오류라면 삭제하거나 수정합니다. 의미 있는 데이터라면 별도로 분석하거나 로그 변환 등으로 영향을 줄일 수 있습니다.

무조건 삭제하면 안 됩니다. VIP 고객의 대량 주문이 이상치로 탐지되었다고 해서 삭제하면, 중요한 비즈니스 인사이트를 놓칠 수 있습니다.

김데이터 씨가 이상치를 확인해봤습니다. 대부분 입력 오류였지만, 한 건은 실제로 대기업의 대량 주문이었습니다.

"이건 삭제하면 안 되겠네요!" 박분석 씨가 고개를 끄덕였습니다. "맞아요.

이상치 탐지는 시작일 뿐이고, 각각을 확인해서 적절히 처리하는 게 중요해요."

실전 팁

💡 - 1.5 × IQR 대신 3 × IQR을 사용하면 더 보수적으로 탐지합니다

  • 도메인 지식을 활용해서 물리적으로 불가능한 값(음수 나이, 200세 등)도 확인하세요
  • 여러 컬럼의 이상치를 동시에 확인하려면 각 컬럼마다 따로 계산하세요

6. 박스플롯으로 이상치 시각화

이상치를 숫자로 확인한 김데이터 씨에게 박분석 씨가 제안했습니다. "숫자만 보면 감이 안 오죠?

박스플롯으로 그려보면 한눈에 이해할 수 있어요." 김데이터 씨는 시각화라는 말에 눈이 반짝였습니다. 그래프로 보면 훨씬 직관적일 것 같습니다.

박스플롯은 데이터의 분포와 이상치를 시각적으로 보여주는 그래프입니다. 마치 데이터를 상자에 담아서 한눈에 보는 것과 같습니다.

상자 안에는 중간 50%의 데이터가 들어가고, 상자 밖의 점들은 이상치를 나타냅니다. matplotlibseaborn 라이브러리로 쉽게 그릴 수 있습니다.

다음 코드를 살펴봅시다.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# 한글 폰트 설정 (맑은 고딕)
plt.rcParams['font.family'] = 'Malgun Gothic'
plt.rcParams['axes.unicode_minus'] = False

# 샘플 데이터 생성
np.random.seed(42)
normal_data = np.random.normal(500000, 100000, 95)
outliers = [5000000, 8000000, 100000]
amounts = np.concatenate([normal_data, outliers])
df = pd.DataFrame({'주문금액': amounts})

# 박스플롯 그리기
plt.figure(figsize=(10, 6))
sns.boxplot(y=df['주문금액'])
plt.title('주문금액 분포 및 이상치', fontsize=14)
plt.ylabel('금액 (원)', fontsize=12)
plt.show()

김데이터 씨는 지금까지 숫자와 코드로만 이상치를 확인했습니다. 하지만 "하한선 30만 원, 상한선 70만 원"이라는 숫자만으로는 감이 잘 오지 않습니다.

시각화가 필요합니다. 박스플롯은 왜 유용할까요?

쉽게 비유하자면, 박스플롯은 마치 학급 성적표를 한눈에 보여주는 그래프와 같습니다. 가운데 선은 중간값, 상자는 중간 50% 학생들의 점수 범위, 그리고 상자 밖의 점들은 유난히 높거나 낮은 점수를 받은 학생들입니다.

박스플롯의 구성 요소를 이해해봅시다. 박스플롯은 크게 다섯 부분으로 이루어집니다.

첫째, 중간선은 중앙값(median)을 나타냅니다. 데이터를 크기 순으로 나열했을 때 정중앙의 값입니다.

둘째, 상자의 아래쪽은 Q1(25% 지점), 상자의 위쪽은 Q3(75% 지점)입니다. 상자 안에는 중간 50%의 데이터가 들어갑니다.

셋째, **수염(whisker)**은 상자에서 위아래로 뻗은 선입니다. 보통 Q1 - 1.5 × IQR부터 Q3 + 1.5 × IQR까지 그려집니다.

넷째, 수염 밖의 점들이 바로 이상치입니다. 위의 코드를 단계별로 살펴보겠습니다.

먼저 한글 폰트를 설정했습니다. matplotlib은 기본적으로 한글을 지원하지 않아서 글자가 깨질 수 있습니다.

**plt.rcParams['font.family'] = 'Malgun Gothic'**으로 맑은 고딕을 설정하면 한글이 제대로 출력됩니다. axes.unicode_minus = False는 마이너스 기호가 깨지지 않도록 합니다.

샘플 데이터는 앞에서 사용한 것과 같습니다. 정규분포를 따르는 95개의 정상 데이터와 3개의 극단적인 이상치를 합쳤습니다.

**sns.boxplot(y=df['주문금액'])**이 핵심입니다. seaborn 라이브러리의 boxplot 함수는 데이터를 받아서 자동으로 박스플롯을 그려줍니다.

y 매개변수에 컬럼을 지정하면 세로 방향 박스플롯이 생성됩니다. x를 사용하면 가로 방향입니다.

**plt.figure(figsize=(10, 6))**은 그래프 크기를 설정합니다. 가로 10인치, 세로 6인치입니다.

**plt.title()**과 **plt.ylabel()**로 제목과 축 레이블을 추가합니다. **plt.show()**를 실행하면 그래프가 화면에 표시됩니다.

Jupyter Notebook에서는 자동으로 표시되지만, 일반 Python 스크립트에서는 **show()**를 명시적으로 호출해야 합니다. 그래프를 보면 무엇을 알 수 있을까요?

상자가 대략 40만~60만 원 사이에 있고, 중간선은 50만 원 근처에 있습니다. 수염은 위아래로 조금 더 뻗어 있습니다.

그리고 상자 위쪽 멀리 떨어진 곳에 세 개의 점이 찍혀 있습니다. 바로 500만 원, 800만 원, 1000만 원대의 이상치들입니다.

숫자로만 "이상치가 3개 있습니다"라고 하는 것보다, 이렇게 그래프로 보면 훨씬 직관적입니다. 이상치가 정말 동떨어져 있다는 것을 한눈에 알 수 있습니다.

실제 프로젝트에서는 어떻게 활용할까요? 예를 들어 여러 제품의 가격 분포를 비교한다고 가정해봅시다.

**sns.boxplot(x='제품명', y='가격', data=df)**처럼 사용하면 제품별로 박스플롯을 나란히 그릴 수 있습니다. 어떤 제품이 가격 편차가 크고, 어떤 제품에 이상치가 많은지 한눈에 비교할 수 있습니다.

또는 월별 판매량을 박스플롯으로 그려서 계절성을 확인할 수도 있습니다. 12개월의 박스플롯을 나란히 그리면 어느 달에 판매량이 높고, 어느 달에 이상치가 많은지 알 수 있습니다.

다른 시각화 방법도 있습니다. 히스토그램은 데이터의 분포를 막대그래프로 보여줍니다.

바이올린 플롯은 박스플롯과 히스토그램을 합친 형태로, 데이터의 밀도까지 보여줍니다. 상황에 따라 적절한 시각화 방법을 선택하면 됩니다.

하지만 박스플롯은 가장 간단하면서도 많은 정보를 제공합니다. 중앙값, 사분위수, 범위, 이상치를 모두 하나의 그래프에서 볼 수 있습니다.

김데이터 씨가 박스플롯을 그려봤습니다. 화면에 깔끔한 그래프가 나타났고, 이상치가 눈에 확 들어왔습니다.

"와! 이렇게 보니까 정말 이해하기 쉽네요!" 박분석 씨가 미소를 지었습니다.

"데이터 분석가는 숫자만 다루는 게 아니라, 다른 사람에게 설명할 수 있어야 해요. 시각화는 그래서 중요하죠."

실전 팁

💡 - seabornswarmplot이나 stripplotboxplot과 겹쳐 그리면 개별 데이터 포인트도 함께 볼 수 있습니다

  • **plt.savefig('boxplot.png')**로 그래프를 파일로 저장할 수 있습니다
  • 여러 컬럼을 동시에 비교하려면 **sns.boxplot(data=df)**처럼 전체 데이터프레임을 전달하세요

7. 데이터 품질 리포트 작성

모든 분석을 마친 김데이터 씨가 박분석 씨에게 물었습니다. "이제 결측치, 중복, 이상치를 다 확인했는데 이걸 어떻게 정리하죠?" 박분석 씨가 대답했습니다.

"마지막 단계는 데이터 품질 리포트를 작성하는 거예요. 한눈에 볼 수 있게 정리하면 됩니다."

데이터 품질 리포트는 결측치, 중복, 이상치 등 모든 품질 지표를 종합한 문서입니다. 마치 건강검진 결과지처럼 데이터의 상태를 한눈에 보여줍니다.

리포트를 작성하면 데이터 수집 프로세스의 문제점을 파악하고, 개선 방향을 제시할 수 있습니다. 데이터 분석 프로젝트의 첫 단계이자 필수 과정입니다.

다음 코드를 살펴봅시다.

import pandas as pd
import numpy as np

# 샘플 데이터 생성
np.random.seed(42)
data = {
    'user_id': range(1, 101),
    'name': ['User' + str(i) if i % 10 != 0 else None for i in range(1, 101)],
    'age': [np.random.randint(20, 60) if i % 15 != 0 else None for i in range(1, 101)],
    'email': ['user' + str(i) + '@email.com' if i % 8 != 0 else None for i in range(1, 101)]
}
df = pd.DataFrame(data)

# 데이터 품질 리포트 생성
def create_quality_report(df):
    report = {}
    report['총 행 수'] = len(df)
    report['총 컬럼 수'] = len(df.columns)
    report['결측치 비율(%)'] = (df.isnull().sum() / len(df) * 100).to_dict()
    report['중복 행 수'] = df.duplicated().sum()
    report['메모리 사용량(MB)'] = df.memory_usage(deep=True).sum() / 1024**2
    return report

# 리포트 출력
quality_report = create_quality_report(df)
print("=" * 50)
print("데이터 품질 리포트")
print("=" * 50)
for key, value in quality_report.items():
    print(f"{key}: {value}")

김데이터 씨는 지금까지 배운 모든 기법을 사용해서 데이터를 분석했습니다. 결측치도 확인하고, 중복도 찾아내고, 이상치도 시각화했습니다.

하지만 이 모든 정보가 여기저기 흩어져 있습니다. 데이터 품질 리포트는 왜 필요할까요?

쉽게 비유하자면, 데이터 품질 리포트는 마치 자동차 정비소에서 받는 점검 리포트와 같습니다. 엔진 상태, 타이어 마모도, 브레이크 성능 등을 한 장의 종이에 정리해서 보여줍니다.

운전자는 이것을 보고 어떤 부분을 수리해야 할지 바로 알 수 있습니다. 데이터 분석에서도 마찬가지입니다.

리포트를 보면 어떤 컬럼에 문제가 있고, 전체 데이터 품질이 어느 수준인지 한눈에 파악할 수 있습니다. 데이터 품질 리포트에는 무엇이 들어가야 할까요?

기본적으로 전체 데이터 규모(행 수, 컬럼 수), 결측치 현황(컬럼별 결측치 비율), 중복 현황(중복 행 개수), 이상치 현황(컬럼별 이상치 개수), 데이터 타입(각 컬럼의 자료형), 메모리 사용량 등이 포함됩니다. 추가로 도메인에 따라 특수한 지표를 넣을 수도 있습니다.

예를 들어 시계열 데이터라면 시간 간격의 일관성, 고객 데이터라면 유효한 이메일 주소 비율 등을 포함할 수 있습니다. 위의 코드를 단계별로 살펴보겠습니다.

먼저 샘플 데이터를 만들었습니다. 100명의 사용자 데이터이고, name, age, email 컬럼마다 의도적으로 결측치를 넣었습니다.

name은 10%의 결측치, age는 약 7%, email은 12.5%의 결측치가 있습니다. create_quality_report() 함수는 데이터프레임을 받아서 품질 리포트를 딕셔너리 형태로 반환합니다.

**len(df)**로 총 행 수를 계산하고, **len(df.columns)**로 총 컬럼 수를 계산합니다. df.isnull().sum() / len(df) * 100은 각 컬럼의 결측치 비율을 백분율로 계산합니다.

**to_dict()**를 사용해서 딕셔너리로 변환하면 리포트에 포함시키기 쉽습니다. **df.duplicated().sum()**은 중복 행의 개수를 계산합니다.

**df.memory_usage(deep=True).sum()**은 데이터프레임이 사용하는 메모리를 바이트 단위로 계산합니다. 1024로 두 번 나누면 메가바이트 단위로 변환됩니다.

마지막으로 리포트를 출력합니다. "=" * 50은 구분선을 그리는 간단한 방법입니다.

for 루프로 딕셔너리의 각 항목을 출력합니다. 실제 프로젝트에서는 어떻게 활용할까요?

대규모 데이터 파이프라인을 운영한다고 가정해봅시다. 매일 밤 새로운 데이터가 수집되는데, 데이터 품질 리포트를 자동으로 생성해서 이메일로 보내거나 대시보드에 표시할 수 있습니다.

만약 결측치 비율이 갑자기 50%로 급증하면 데이터 수집 시스템에 문제가 생긴 것입니다. 즉시 대응할 수 있습니다.

또는 머신러닝 프로젝트를 시작할 때, 데이터 품질 리포트를 먼저 작성하면 데이터 전처리 전략을 세울 수 있습니다. 결측치가 많은 컬럼은 삭제하거나 대체할 방법을 고민하고, 중복이 많으면 정제 프로세스를 추가합니다.

리포트를 더 풍부하게 만들 수도 있습니다. pandas-profiling 라이브러리를 사용하면 자동으로 상세한 HTML 리포트를 생성할 수 있습니다.

각 컬럼의 분포, 상관관계, 이상치 등을 시각화해서 보여줍니다. 하지만 기본적인 리포트는 위의 코드처럼 직접 만드는 것이 더 유연합니다.

리포트를 Markdown 파일이나 HTML 파일로 저장하면 다른 사람과 공유하기 쉽습니다. PDF로 출력해서 보고서에 첨부할 수도 있습니다.

주의할 점도 있습니다. 리포트는 정기적으로 생성해야 의미가 있습니다.

한 번 만들고 끝이 아니라, 데이터가 업데이트될 때마다 새로 만들어서 품질 변화를 추적해야 합니다. 또한 리포트만 보고 끝내지 말고, 문제점을 발견하면 즉시 조치해야 합니다.

김데이터 씨가 리포트를 생성해봤습니다. 깔끔하게 정리된 품질 지표가 화면에 출력되었습니다.

"이제 팀장님께 이 리포트를 보여드리면 되겠네요!" 박분석 씨가 고개를 끄덕였습니다. "맞아요.

데이터 분석가의 첫 번째 일은 데이터를 이해하고, 품질을 확인하는 거예요. 이제 김데이터 씨도 제대로 된 데이터 분석가가 됐네요!" 김데이터 씨는 뿌듯한 마음으로 리포트를 저장했습니다.

결측치, 중복, 이상치를 확인하는 방법을 익혔고, 이제 어떤 데이터를 받아도 품질을 평가할 자신이 생겼습니다. 데이터 품질 확인은 모든 데이터 분석의 출발점입니다.

여러분도 오늘 배운 기법들을 실제 프로젝트에 적용해 보세요. 깨끗한 데이터가 정확한 분석의 기초입니다.

실전 팁

💡 - pandas-profiling 라이브러리로 자동화된 리포트를 생성할 수 있습니다

  • 리포트를 함수로 만들어서 재사용하세요
  • 시간별/날짜별로 리포트를 저장해서 품질 추이를 추적하세요

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

#Python#Pandas#DataQuality#NullCheck#Outliers

댓글 (0)

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