이미지 로딩 중...

데이터 품질 체크 완벽 가이드 결측치와 중복 확인 - 슬라이드 1/9
A

AI Generated

2025. 11. 24. · 4 Views

데이터 품질 체크 완벽 가이드 결측치와 중복 확인

실무 데이터 분석에서 가장 먼저 해야 할 데이터 품질 확인 방법을 알아봅니다. 결측치와 중복 데이터를 찾고 처리하는 실전 기법을 pandas를 활용하여 단계별로 배워보세요.


목차

  1. 결측치_확인하기
  2. 결측치_처리_삭제하기
  3. 결측치_처리_채우기
  4. 중복_데이터_확인하기
  5. 중복_데이터_제거하기
  6. 데이터_타입_확인과_변환
  7. 이상치_탐지하기
  8. 데이터_분포_확인하기

1. 결측치_확인하기

시작하며

여러분이 고객 데이터를 분석하려고 CSV 파일을 열었는데, 어떤 행에는 이메일이 비어있고, 어떤 행에는 전화번호가 없는 상황을 겪어본 적 있나요? "이 데이터로 분석을 해도 될까?" 하는 불안감이 들 겁니다.

이런 문제는 실제 개발 현장에서 매일 발생합니다. 결측치(Missing Data)가 있는 상태로 분석을 진행하면 잘못된 결과가 나오거나, 심지어 프로그램이 에러를 일으킬 수 있습니다.

예를 들어, 평균을 계산할 때 빈 값이 섞여 있으면 예상과 다른 숫자가 나오게 됩니다. 바로 이럴 때 필요한 것이 결측치 확인입니다.

데이터의 어느 부분이 비어있는지 정확히 파악해야, 그 다음 단계로 채우거나 삭제하는 등의 조치를 취할 수 있습니다.

개요

간단히 말해서, 결측치 확인은 데이터셋에서 빈 값(NaN, None, null 등)이 어디에 얼마나 있는지 찾아내는 작업입니다. 실무에서 데이터를 받으면 가장 먼저 해야 하는 일이 바로 이것입니다.

고객이 설문조사에서 일부 항목을 건너뛰었거나, 센서가 일시적으로 작동하지 않아서 측정값이 없거나, 데이터베이스에서 값이 입력되지 않은 경우 등 결측치가 생기는 이유는 다양합니다. 예를 들어, 온라인 쇼핑몰 데이터를 분석할 때 리뷰 점수가 없는 상품이 30%나 된다면, 이를 모르고 평균 점수를 계산하면 왜곡된 결과가 나옵니다.

전통적인 방법으로는 엑셀에서 일일이 스크롤하며 빈 셀을 찾았다면, 이제는 pandas의 isnull(), isna(), info() 같은 메서드로 단 몇 줄의 코드로 전체 데이터셋의 결측치를 한눈에 파악할 수 있습니다. 핵심 특징은 첫째, 각 컬럼별로 결측치 개수를 알 수 있고, 둘째, 전체 데이터 중 몇 퍼센트가 결측치인지 비율도 계산할 수 있으며, 셋째, 시각화를 통해 결측치 패턴을 파악할 수 있다는 점입니다.

이러한 특징들이 데이터 품질을 객관적으로 평가하고 다음 처리 방향을 결정하는 데 매우 중요합니다.

코드 예제

import pandas as pd
import numpy as np

# 샘플 데이터프레임 생성
data = {
    'name': ['김철수', '이영희', np.nan, '박민수', '최지영'],
    'age': [25, np.nan, 35, 28, np.nan],
    'email': ['kim@example.com', None, 'park@example.com', np.nan, 'choi@example.com'],
    'phone': ['010-1234-5678', '010-2345-6789', np.nan, '010-4567-8901', '010-5678-9012']
}
df = pd.DataFrame(data)

# 결측치 확인 방법 1: 각 컬럼별 결측치 개수
print("결측치 개수:")
print(df.isnull().sum())

# 결측치 확인 방법 2: 결측치 비율(%)
print("\n결측치 비율(%):")
print(df.isnull().sum() / len(df) * 100)

# 결측치 확인 방법 3: 데이터프레임 전체 정보
print("\n데이터프레임 정보:")
df.info()

설명

이 코드가 하는 일은 데이터프레임의 각 컬럼에서 비어있는 값들을 찾아내고, 그 개수와 비율을 계산하여 보여주는 것입니다. 첫 번째 단계에서 df.isnull()은 데이터프레임의 모든 셀을 검사하여 True(결측치)/False(정상값)로 이루어진 같은 크기의 데이터프레임을 반환합니다.

여기에 .sum()을 붙이면 각 컬럼별로 True의 개수, 즉 결측치의 개수를 세어줍니다. 왜 이렇게 하냐면, pandas에서 True는 1로, False는 0으로 계산되기 때문에 sum()으로 합계를 내면 자동으로 결측치 개수가 나오는 것이죠.

두 번째 단계에서는 같은 방식으로 결측치 개수를 구한 뒤, 전체 행 개수(len(df))로 나누고 100을 곱하여 퍼센트로 변환합니다. 예를 들어, 100개의 데이터 중 15개가 비어있다면 15%라고 표시됩니다.

이렇게 비율로 보면 "이 컬럼의 30%가 비어있으니 이 컬럼은 분석에 사용하기 어렵겠다"는 식으로 빠르게 판단할 수 있습니다. 세 번째 단계의 df.info()는 데이터프레임의 전반적인 정보를 요약해줍니다.

각 컬럼의 데이터 타입, non-null 값의 개수(즉, 결측치가 아닌 값의 개수), 메모리 사용량 등을 한 번에 보여주죠. 특히 "5 entries" 중 "4 non-null"이라고 표시되면, 한눈에 1개가 결측치임을 알 수 있어서 매우 편리합니다.

여러분이 이 코드를 사용하면 1,000행이든 100만 행이든 단 1초 만에 모든 컬럼의 결측치 상태를 파악할 수 있습니다. 실무에서는 이 정보를 바탕으로 "age 컬럼의 결측치는 평균값으로 채우고, email 컬럼의 결측치가 있는 행은 삭제하자"는 식으로 데이터 전처리 계획을 세우게 됩니다.

또한 고객에게 "수집하신 데이터의 20%에 결측치가 있어서 추가 수집이 필요합니다"라고 보고할 수도 있죠.

실전 팁

💡 isnull()isna()는 완전히 같은 기능을 하므로 둘 중 편한 것을 사용하면 됩니다. 팀에서 어느 쪽을 더 선호하는지 확인하고 일관성 있게 사용하세요.

💡 결측치 비율이 70% 이상인 컬럼은 대부분의 경우 분석에서 제외하는 것이 좋습니다. 정보가 너무 부족해서 의미 있는 결과를 얻기 어렵기 때문입니다.

💡 df.isnull().sum().sum()처럼 sum()을 두 번 사용하면 전체 데이터프레임에서 결측치의 총 개수를 알 수 있어요. 전체적인 데이터 품질을 빠르게 판단할 때 유용합니다.

💡 시간 관련 데이터에서 결측치가 연속으로 나타난다면 센서 고장이나 시스템 다운타임을 의미할 수 있으니, 단순히 채우지 말고 원인을 먼저 파악하세요.

💡 결측치 패턴을 시각적으로 보고 싶다면 missingno 라이브러리를 설치하여 msno.matrix(df)를 사용해보세요. 어떤 행에 결측치가 집중되어 있는지 한눈에 볼 수 있습니다.


2. 결측치_처리_삭제하기

시작하며

여러분이 결측치를 확인했는데, 생각보다 많지 않다면 어떻게 하시겠어요? "그냥 그 행들을 빼버리면 되지 않을까?"라고 생각할 수 있습니다.

맞습니다! 이것이 가장 간단하고 확실한 방법 중 하나입니다.

이런 상황은 실제로 자주 발생합니다. 예를 들어, 10,000개의 설문 데이터 중 50개만 중요한 질문에 답하지 않았다면, 그 50개를 제외하고도 충분히 의미 있는 분석이 가능합니다.

하지만 주의할 점도 있습니다. 만약 결측치가 있는 행을 무작정 삭제했는데 전체 데이터의 절반이 날아가버린다면, 분석 자체가 불가능해질 수 있습니다.

바로 이럴 때 필요한 것이 결측치 삭제 전략입니다. 언제 삭제해도 되고, 어떤 방식으로 삭제해야 하는지를 알아야 데이터 손실을 최소화하면서 품질을 높일 수 있습니다.

개요

간단히 말해서, 결측치 삭제는 비어있는 값이 포함된 행이나 컬럼을 데이터셋에서 제거하는 작업입니다. 이 방법은 결측치가 전체 데이터의 5% 이하이고, 랜덤하게 분포되어 있을 때 가장 안전합니다.

만약 특정 그룹(예: 고소득층)에서만 결측치가 집중되어 있다면, 삭제 시 분석 결과가 편향될 수 있으니 조심해야 합니다. 예를 들어, 건강검진 데이터에서 혈압 정보가 없는 100명을 삭제했는데, 알고 보니 그들이 모두 고혈압으로 병원에 오지 않은 사람들이었다면, 전체 평균 혈압이 왜곡되는 거죠.

기존에는 엑셀에서 필터를 걸어서 빈 셀이 있는 행을 찾아 수동으로 삭제했다면, 이제는 pandas의 dropna() 메서드 하나로 다양한 조건의 삭제 작업을 자동화할 수 있습니다. 핵심 특징은 첫째, 행 단위 삭제와 열 단위 삭제를 선택할 수 있고, 둘째, "모든 값이 결측치인 경우만" 또는 "하나라도 결측치가 있으면" 같은 조건을 설정할 수 있으며, 셋째, 특정 컬럼만 기준으로 삼을 수도 있다는 점입니다.

이러한 유연성이 실무에서 다양한 상황에 대응할 수 있게 해줍니다.

코드 예제

import pandas as pd
import numpy as np

# 샘플 데이터
df = pd.DataFrame({
    'id': [1, 2, 3, 4, 5],
    'name': ['김철수', np.nan, '박민수', '최지영', '정다은'],
    'age': [25, 30, np.nan, 28, 32],
    'score': [85, 90, 88, np.nan, 95]
})

print("원본 데이터:")
print(df)

# 방법 1: 하나라도 결측치가 있는 행 삭제
df_drop_any = df.dropna()
print("\n하나라도 결측치 있는 행 삭제:")
print(df_drop_any)

# 방법 2: 모든 값이 결측치인 행만 삭제
df_drop_all = df.dropna(how='all')
print("\n모든 값이 결측치인 행만 삭제:")
print(df_drop_all)

# 방법 3: 특정 컬럼(name)만 기준으로 삭제
df_drop_subset = df.dropna(subset=['name'])
print("\nname 컬럼이 결측치인 행만 삭제:")
print(df_drop_subset)

# 방법 4: 결측치가 2개 이상인 행 삭제
df_drop_thresh = df.dropna(thresh=3)  # 최소 3개의 non-null 값이 있어야 유지
print("\n유효한 값이 3개 미만인 행 삭제:")
print(df_drop_thresh)

설명

이 코드가 하는 일은 다양한 조건에 따라 결측치가 포함된 행을 선택적으로 제거하여 깨끗한 데이터셋을 만드는 것입니다. 첫 번째 방법인 dropna()는 기본적으로 how='any' 옵션이 적용되어, 행에 하나라도 NaN이 있으면 그 행 전체를 삭제합니다.

예를 들어, 이름, 나이, 점수 중 하나라도 비어있으면 그 사람의 모든 정보를 버리는 거죠. 가장 엄격한 방법이라서 데이터가 많이 줄어들 수 있지만, 완벽한 데이터만 남게 됩니다.

두 번째 방법에서 how='all'을 사용하면 행의 모든 값이 NaN일 때만 삭제합니다. 예를 들어, 이름도 나이도 점수도 모두 비어있는 완전히 빈 행만 제거하는 거예요.

실무에서는 데이터 수집 과정에서 빈 행이 실수로 들어가는 경우가 있는데, 이럴 때 유용합니다. 일부만 채워진 데이터라도 가치가 있다면 남겨두는 것이죠.

세 번째 방법의 subset=['name']은 특정 컬럼만을 기준으로 삼습니다. 전체 데이터에서 결측치가 있더라도, name 컬럼이 채워져 있으면 그 행을 유지합니다.

예를 들어, 고객 분석에서 이름은 필수이지만 전화번호는 선택사항이라면, 이름이 없는 행만 삭제하고 전화번호가 없는 행은 살려둘 수 있습니다. 네 번째 방법인 thresh=3은 "최소 3개의 유효한 값이 있어야 한다"는 의미입니다.

4개 컬럼이 있는데 thresh=3이면, 최소 3개는 채워져 있어야 하니 결측치가 2개 이상인 행은 삭제됩니다. 이 방식은 "정보가 너무 부족한 행만 걸러내고 싶다"는 상황에 딱 맞습니다.

여러분이 이 코드를 사용하면 상황에 맞게 결측치 삭제 강도를 조절할 수 있습니다. 데이터가 풍부하면 엄격하게, 데이터가 귀하면 느슨하게 적용하는 거죠.

실무에서는 삭제 전후의 데이터 개수를 비교하여 "원본 10,000개 중 8,500개를 유지했습니다"라고 보고하는 것도 중요합니다.

실전 팁

💡 dropna(inplace=True)를 사용하면 원본 데이터프레임이 직접 수정되므로, 나중에 되돌릴 수 없습니다. 안전하게 df_clean = df.dropna()처럼 새 변수에 저장하는 것을 추천합니다.

💡 삭제하기 전에 print(f"삭제 전: {len(df)}개, 삭제 후: {len(df_clean)}개")로 얼마나 많은 데이터가 사라지는지 확인하세요. 50% 이상 줄어든다면 삭제 대신 다른 방법을 고려해야 합니다.

💡 axis=1을 추가하면 행이 아니라 컬럼(열)을 삭제합니다. 예를 들어, 어떤 컬럼의 80%가 비어있다면 그 컬럼 자체를 없애는 것이 나을 수 있습니다.

💡 삭제된 행의 인덱스가 유지되어 0, 1, 3, 5처럼 건너뛰게 되는데, 이게 불편하다면 .reset_index(drop=True)로 인덱스를 다시 0부터 순차적으로 만들 수 있습니다.

💡 타임시리즈 데이터에서는 결측치 삭제보다 보간(interpolation)이 더 적합할 수 있습니다. 예를 들어, 온도 센서 데이터에서 중간 값이 없다면 전후 값의 평균으로 채우는 것이 더 현실적이죠.


3. 결측치_처리_채우기

시작하며

여러분이 소중한 데이터를 삭제하기가 아깝다면 어떻게 할까요? 예를 들어, 1년간 수집한 100명의 설문 데이터 중 20명이 나이를 적지 않았는데, 이 20명을 버리면 다른 중요한 정보까지 잃게 됩니다.

이런 딜레마는 데이터 분석가들이 항상 마주하는 문제입니다. 결측치를 삭제하면 데이터가 줄어들어 분석의 정확도가 떨어지고, 그렇다고 비워두면 계산이 불가능하거나 에러가 발생합니다.

특히 머신러닝 모델은 대부분 결측치를 허용하지 않아서, 반드시 채우거나 삭제해야 합니다. 바로 이럴 때 필요한 것이 결측치 채우기(Imputation)입니다.

합리적인 값으로 빈 곳을 메워서 데이터 손실 없이 분석을 진행할 수 있게 해줍니다.

개요

간단히 말해서, 결측치 채우기는 비어있는 값을 특정 규칙이나 통계값으로 대체하는 작업입니다. 실무에서는 데이터의 특성에 따라 채우는 방법을 다르게 선택합니다.

숫자 데이터라면 평균값, 중앙값, 최빈값을 사용하고, 범주형 데이터라면 가장 빈번한 값을 사용합니다. 예를 들어, 고객의 나이가 비어있다면 전체 고객의 평균 나이로 채우는 것이 합리적입니다.

하지만 VIP 고객 여부(Yes/No)가 비어있다면, 대다수가 일반 고객이므로 'No'로 채우는 것이 현실적이죠. 전통적으로는 결측치를 찾아서 일일이 수동으로 값을 입력했다면, 이제는 pandas의 fillna(), ffill(), bfill() 같은 메서드로 수천 개의 결측치를 한 번에 자동으로 채울 수 있습니다.

핵심 특징은 첫째, 단순한 고정값부터 복잡한 통계값까지 다양한 방법을 선택할 수 있고, 둘째, 앞의 값이나 뒤의 값으로 채우는 순차적 방법도 가능하며, 셋째, 컬럼별로 다른 채우기 방법을 적용할 수도 있다는 점입니다. 이러한 유연성이 데이터의 특성을 최대한 유지하면서 결측치를 처리할 수 있게 해줍니다.

코드 예제

import pandas as pd
import numpy as np

# 샘플 데이터
df = pd.DataFrame({
    'name': ['김철수', '이영희', '박민수', '최지영', '정다은'],
    'age': [25, np.nan, 35, np.nan, 30],
    'city': ['서울', '부산', np.nan, '대구', np.nan],
    'score': [85, 90, np.nan, 88, 92]
})

print("원본 데이터:")
print(df)

# 방법 1: 특정 값으로 채우기
df_fill_value = df.fillna({'age': 0, 'city': '미정', 'score': 0})
print("\n특정 값으로 채우기:")
print(df_fill_value)

# 방법 2: 평균값으로 채우기 (숫자 컬럼)
df_fill_mean = df.copy()
df_fill_mean['age'] = df_fill_mean['age'].fillna(df['age'].mean())
df_fill_mean['score'] = df_fill_mean['score'].fillna(df['score'].mean())
print("\n평균값으로 채우기:")
print(df_fill_mean)

# 방법 3: 중앙값으로 채우기
df_fill_median = df.copy()
df_fill_median['age'] = df_fill_median['age'].fillna(df['age'].median())
print("\n중앙값으로 채우기:")
print(df_fill_median)

# 방법 4: 앞의 값으로 채우기 (Forward Fill)
df_ffill = df.fillna(method='ffill')
print("\n앞의 값으로 채우기:")
print(df_ffill)

# 방법 5: 뒤의 값으로 채우기 (Backward Fill)
df_bfill = df.fillna(method='bfill')
print("\n뒤의 값으로 채우기:")
print(df_bfill)

설명

이 코드가 하는 일은 결측치를 상황에 맞는 합리적인 값으로 대체하여 완전한 데이터셋을 만드는 것입니다. 첫 번째 방법에서 fillna({'age': 0, 'city': '미정'})처럼 딕셔너리를 사용하면 각 컬럼마다 다른 값으로 채울 수 있습니다.

age는 0으로, city는 '미정'이라는 문자열로 채우는 거죠. 이 방법은 빠르고 간단하지만, 0이나 '미정' 같은 임의의 값이 실제 분석에서 오해를 불러일으킬 수 있으니 주의해야 합니다.

두 번째 방법인 평균값 채우기는 숫자 데이터에서 가장 흔히 사용됩니다. df['age'].mean()으로 전체 나이의 평균을 계산한 후, 그 값으로 빈 곳을 채우는 거예요.

예를 들어, 5명의 나이가 25, 30, 35라면 평균이 30이므로 결측치를 30으로 채웁니다. 이렇게 하면 전체 평균이 크게 변하지 않아서 통계적으로 안정적입니다.

하지만 모든 결측치가 같은 값(30)이 되니, 분산이 줄어드는 단점도 있습니다. 세 번째 방법인 중앙값 채우기는 이상치(outlier)가 있을 때 평균보다 안전합니다.

예를 들어, 나이가 20, 25, 30, 100이라면 평균은 43.75이지만 중앙값은 27.5로, 중앙값이 더 현실적입니다. 100이라는 극단값이 평균을 왜곡시킨 거죠.

소득, 가격 같은 데이터는 일부 극단값이 있기 때문에 중앙값이 더 적합합니다. 네 번째와 다섯 번째 방법인 ffill()bfill()은 시계열 데이터나 순차적 데이터에서 유용합니다.

ffill(forward fill)은 결측치 바로 위에 있는 값으로 채우고, bfill(backward fill)은 바로 아래 있는 값으로 채웁니다. 예를 들어, 주식 가격이 월요일 100원, 화요일 결측, 수요일 110원이라면, ffill은 화요일을 100원으로, bfill은 110원으로 채웁니다.

"값이 갑자기 변하지 않고 유지된다"는 가정이 합리적인 상황에서 사용하세요. 여러분이 이 코드를 사용하면 데이터를 버리지 않고도 분석을 진행할 수 있습니다.

실무에서는 "age는 중앙값, score는 평균값, city는 최빈값으로 채웠습니다"라고 문서화하여, 나중에 결과를 해석할 때 참고할 수 있게 합니다. 또한 채우기 전후의 통계값(평균, 표준편차 등)을 비교하여 데이터 분포가 크게 변하지 않았는지 확인하는 것도 중요합니다.

실전 팁

💡 평균값으로 채우기 전에 df['age'].describe()로 평균과 표준편차를 확인하세요. 이상치가 있다면 평균 대신 중앙값을 사용하는 것이 더 안전합니다.

💡 fillna(method='ffill', limit=1)처럼 limit 옵션을 주면 연속된 결측치 중 1개만 채우고 나머지는 그대로 둡니다. 너무 많은 연속 결측치를 같은 값으로 채우면 데이터가 왜곡될 수 있으니 조심하세요.

💡 범주형 데이터(성별, 도시 등)는 최빈값으로 채우는 것이 좋습니다. df['city'].mode()[0]로 가장 많이 등장한 값을 찾아서 사용하세요.

💡 결측치를 채운 후에는 df.isnull().sum()으로 정말 모든 결측치가 사라졌는지 다시 확인하세요. 간혹 특정 컬럼을 빠뜨리는 실수가 발생할 수 있습니다.

💡 머신러닝을 할 거라면 sklearn.impute.SimpleImputerKNNImputer 같은 고급 방법도 고려해보세요. 여러 컬럼의 관계를 고려하여 더 정교하게 채울 수 있습니다.


4. 중복_데이터_확인하기

시작하며

여러분이 고객 데이터베이스를 확인했는데, 같은 사람이 두 번 등록되어 있거나, 똑같은 주문 기록이 중복으로 저장된 경우를 본 적 있나요? "이 데이터를 믿어도 될까?" 하는 의심이 들 겁니다.

이런 문제는 실제로 매우 흔하게 발생합니다. 사용자가 회원가입 버튼을 두 번 눌렀거나, 시스템 오류로 같은 데이터가 여러 번 저장되거나, 여러 데이터 소스를 합치는 과정에서 중복이 생길 수 있습니다.

중복 데이터가 있으면 통계가 왜곡되고, 고객 수를 잘못 세거나, 매출을 과대평가하는 등의 문제가 생깁니다. 바로 이럴 때 필요한 것이 중복 데이터 확인입니다.

어떤 행이 중복되었는지 찾아내야 제거하거나 합칠 수 있고, 데이터의 신뢰성을 확보할 수 있습니다.

개요

간단히 말해서, 중복 데이터 확인은 데이터셋에서 완전히 같은 행이나 특정 컬럼이 같은 행을 찾아내는 작업입니다. 실무에서는 중복의 기준이 상황마다 다릅니다.

모든 컬럼이 완전히 같아야 중복인 경우도 있고, 이메일이나 전화번호만 같으면 중복으로 간주하는 경우도 있습니다. 예를 들어, 온라인 쇼핑몰에서 같은 사람이 주소를 약간 다르게 입력해도 이메일이 같다면 동일인으로 봐야겠죠.

반면 실험 데이터에서는 모든 측정값이 완전히 같을 때만 중복으로 처리합니다. 전통적으로는 엑셀에서 정렬하여 눈으로 비교하거나 조건부 서식으로 중복을 찾았다면, 이제는 pandas의 duplicated(), drop_duplicates() 메서드로 수십만 행의 데이터에서도 단 1초 만에 중복을 찾아낼 수 있습니다.

핵심 특징은 첫째, 전체 행 기준 또는 특정 컬럼 기준으로 중복을 판단할 수 있고, 둘째, 중복 중 첫 번째를 남길지 마지막을 남길지 선택할 수 있으며, 셋째, 중복된 행만 따로 추출하여 확인할 수도 있다는 점입니다. 이러한 기능들이 다양한 중복 상황에 유연하게 대응할 수 있게 해줍니다.

코드 예제

import pandas as pd
import numpy as np

# 샘플 데이터 (중복 포함)
df = pd.DataFrame({
    'name': ['김철수', '이영희', '김철수', '박민수', '이영희'],
    'email': ['kim@example.com', 'lee@example.com', 'kim@example.com', 'park@example.com', 'lee@example.com'],
    'age': [25, 30, 25, 35, 30],
    'city': ['서울', '부산', '서울', '대구', '부산']
})

print("원본 데이터:")
print(df)

# 방법 1: 완전히 동일한 행 찾기
print("\n중복 여부 (True=중복):")
print(df.duplicated())

# 방법 2: 중복된 행 개수 세기
print(f"\n중복된 행 개수: {df.duplicated().sum()}개")

# 방법 3: 중복된 행만 추출하여 확인
duplicates = df[df.duplicated(keep=False)]
print("\n중복된 모든 행 (원본 포함):")
print(duplicates)

# 방법 4: 특정 컬럼(email)만 기준으로 중복 찾기
print("\nemail 기준 중복 여부:")
print(df.duplicated(subset=['email']))

# 방법 5: 여러 컬럼(name, age) 조합으로 중복 찾기
print("\nname과 age 조합 기준 중복 여부:")
print(df.duplicated(subset=['name', 'age']))

설명

이 코드가 하는 일은 데이터프레임에서 중복된 행을 다양한 기준으로 찾아내고 표시하는 것입니다. 첫 번째 방법인 df.duplicated()는 각 행이 중복인지 아닌지를 True/False로 알려줍니다.

기본적으로 모든 컬럼이 완전히 같은 행을 찾는데, 첫 번째로 나타난 행은 False(중복 아님), 두 번째부터는 True(중복)로 표시합니다. 예를 들어, "김철수, kim@example.com, 25, 서울"이 첫 번째 행(index 0)과 세 번째 행(index 2)에 있다면, index 0은 False, index 2는 True가 됩니다.

왜냐하면 index 0이 원본이고 index 2가 중복이기 때문이죠. 두 번째 방법에서 .sum()을 붙이면 True의 개수, 즉 중복된 행이 몇 개인지 총계를 알 수 있습니다.

예를 들어, 10,000개 데이터 중 중복이 150개라면 150이 출력되어, "전체의 1.5%가 중복이네"라고 빠르게 파악할 수 있습니다. 세 번째 방법인 df[df.duplicated(keep=False)]는 조금 특별한데, keep=False를 사용하면 중복된 모든 행(원본 포함)을 True로 표시합니다.

즉, index 0과 index 2가 모두 True가 되어 둘 다 출력됩니다. 이렇게 하면 "어떤 행들이 서로 중복인지" 한눈에 비교할 수 있어서 매우 유용합니다.

원본은 괜찮고 중복본만 문제인지, 아니면 둘 다 검토해야 하는지 판단할 수 있죠. 네 번째 방법에서 subset=['email']을 사용하면 전체 컬럼이 아니라 email 컬럼만 비교합니다.

이름이나 나이는 달라도 이메일이 같으면 중복으로 간주하는 거예요. 실무에서 "고객은 이메일로 구분한다"는 비즈니스 규칙이 있다면, 이 방법으로 같은 이메일이 여러 번 등록된 것을 찾아낼 수 있습니다.

다섯 번째 방법은 여러 컬럼을 조합하여 중복을 판단합니다. subset=['name', 'age']는 "이름과 나이가 모두 같아야 중복"이라는 뜻입니다.

예를 들어, 같은 이름인 "김철수"가 두 명 있어도 나이가 25와 30으로 다르면 중복이 아니라고 판단합니다. 이는 주민등록번호나 ID 없이 여러 속성의 조합으로 동일인을 찾을 때 사용합니다.

여러분이 이 코드를 사용하면 데이터의 품질 문제를 조기에 발견할 수 있습니다. 실무에서는 "전체 50,000건 중 1,200건이 이메일 기준으로 중복되었습니다"라고 보고하고, 중복된 행들을 엑셀로 내보내어 수동 검토를 요청하거나, 자동으로 제거하는 등의 조치를 취하게 됩니다.

실전 팁

💡 duplicated(keep='first')는 첫 번째 행을 원본으로, keep='last'는 마지막 행을 원본으로 간주합니다. 시간 순으로 정렬된 데이터라면 가장 최신 것을 남기고 싶을 때 'last'를 사용하세요.

💡 중복 확인 전에 df.sort_values(['name', 'email'])로 정렬하면 중복된 행들이 붙어서 나타나 육안으로도 확인하기 쉽습니다.

💡 df.duplicated().value_counts()를 실행하면 중복이 아닌 행(False)과 중복인 행(True)의 개수를 한 번에 볼 수 있어서 전체 현황 파악에 좋습니다.

💡 대소문자나 공백 차이 때문에 중복을 못 찾는 경우가 있습니다. 확인 전에 df['email'] = df['email'].str.lower().str.strip()으로 소문자 변환과 공백 제거를 해보세요.

💡 중복된 행을 CSV로 저장하고 싶다면 duplicates.to_csv('duplicates.csv', index=False)를 사용하여 관계자와 공유하거나 수동 검토에 활용하세요.


5. 중복_데이터_제거하기

시작하며

여러분이 중복된 데이터를 찾았다면, 다음 단계는 무엇일까요? 당연히 제거하는 것입니다!

하지만 "어떤 것을 남기고 어떤 것을 지울까?"라는 고민이 생깁니다. 실무에서는 중복 제거가 신중해야 합니다.

잘못 제거하면 중요한 정보를 잃을 수 있고, 나중에 복구가 어렵기 때문입니다. 예를 들어, 같은 고객이 여러 번 구매한 기록을 중복이라고 모두 지워버리면, 실제 구매 횟수를 파악할 수 없게 됩니다.

반대로 정말 실수로 두 번 입력된 데이터는 반드시 제거해야 정확한 통계를 낼 수 있죠. 바로 이럴 때 필요한 것이 중복 데이터 제거 전략입니다.

무엇을 기준으로, 무엇을 남기고, 어떻게 제거할지를 명확히 정해야 데이터 무결성을 유지할 수 있습니다.

개요

간단히 말해서, 중복 데이터 제거는 중복으로 판단된 행들 중 하나만 남기고 나머지를 삭제하는 작업입니다. 이 작업은 중복 확인과 마찬가지로 명확한 기준이 필요합니다.

전체 행이 완전히 같은 경우는 안전하게 제거할 수 있지만, 일부 컬럼만 같은 경우는 어느 것을 남길지 비즈니스 로직에 따라 결정해야 합니다. 예를 들어, 같은 이메일로 여러 계정이 있다면 가장 최근에 생성된 계정을 남기거나, 가장 많은 구매 이력이 있는 계정을 남기는 식으로 규칙을 정해야 합니다.

기존에는 중복을 찾아서 하나하나 수동으로 삭제했다면, 이제는 pandas의 drop_duplicates() 메서드 하나로 수천 개의 중복을 자동으로 정리할 수 있습니다. 원본을 보존할지, 마지막을 보존할지도 간단히 설정할 수 있습니다.

핵심 특징은 첫째, 중복 판단 기준(전체 행 또는 특정 컬럼)을 유연하게 설정할 수 있고, 둘째, 어떤 행을 남길지(첫 번째, 마지막, 또는 모두 제거) 선택할 수 있으며, 셋째, 원본을 보존하거나 직접 수정할 수 있다는 점입니다. 이러한 옵션들이 안전하고 정확한 중복 제거를 가능하게 합니다.

코드 예제

import pandas as pd

# 샘플 데이터 (중복 포함)
df = pd.DataFrame({
    'name': ['김철수', '이영희', '김철수', '박민수', '이영희', '최지영'],
    'email': ['kim@example.com', 'lee@example.com', 'kim@example.com', 'park@example.com', 'lee2@example.com', 'choi@example.com'],
    'age': [25, 30, 25, 35, 30, 28],
    'signup_date': ['2024-01-01', '2024-01-05', '2024-01-01', '2024-01-10', '2024-02-01', '2024-01-15']
})

print("원본 데이터:")
print(df)
print(f"원본 행 개수: {len(df)}개\n")

# 방법 1: 완전히 동일한 행 제거 (첫 번째 유지)
df_drop_all = df.drop_duplicates()
print("방법 1 - 완전 중복 제거 (첫 번째 유지):")
print(df_drop_all)
print(f"제거 후 행 개수: {len(df_drop_all)}개\n")

# 방법 2: 특정 컬럼(name) 기준 중복 제거
df_drop_name = df.drop_duplicates(subset=['name'])
print("방법 2 - name 기준 중복 제거:")
print(df_drop_name)
print(f"제거 후 행 개수: {len(df_drop_name)}개\n")

# 방법 3: 마지막 행 유지하며 중복 제거
df_drop_last = df.drop_duplicates(subset=['name'], keep='last')
print("방법 3 - name 기준 중복 제거 (마지막 유지):")
print(df_drop_last)

# 방법 4: 중복된 행 모두 제거 (원본도 제거)
df_drop_false = df.drop_duplicates(subset=['name'], keep=False)
print("\n방법 4 - name 기준 중복 모두 제거:")
print(df_drop_false)

설명

이 코드가 하는 일은 중복된 행들 중에서 특정 규칙에 따라 하나만 남기거나 모두 제거하여 깨끗한 데이터셋을 만드는 것입니다. 첫 번째 방법인 drop_duplicates()는 기본 설정으로, 모든 컬럼이 완전히 같은 행을 찾아서 첫 번째 행만 남기고 나머지를 삭제합니다.

예를 들어, "김철수, kim@example.com, 25, 2024-01-01"이 index 0과 index 2에 있다면, index 0만 남고 index 2는 사라집니다. 이 방법은 데이터가 실수로 두 번 입력된 경우에 가장 안전하게 사용할 수 있습니다.

두 번째 방법에서 subset=['name']을 사용하면 이름만 기준으로 중복을 제거합니다. "김철수"가 두 행에 있으면, 다른 정보(이메일, 나이 등)가 달라도 첫 번째 "김철수"만 남기고 두 번째는 삭제합니다.

실무에서 "고객명으로 대표 레코드를 선정한다"는 규칙이 있을 때 유용합니다. 하지만 주의할 점은, 동명이인일 가능성도 있으니 name만으로는 부족하고 email이나 전화번호를 함께 사용하는 것이 더 안전합니다.

세 번째 방법인 keep='last'는 중복 중 마지막 행을 남깁니다. 예를 들어, 데이터가 시간 순으로 정렬되어 있고 signup_date가 더 최신인 행을 남기고 싶다면, 먼저 df.sort_values('signup_date')로 정렬한 후 keep='last'를 사용하면 가장 최신 정보만 남게 됩니다.

고객 정보가 업데이트되면서 중복이 생긴 경우, 최신 정보가 더 정확할 가능성이 높으니까요. 네 번째 방법인 keep=False는 조금 극단적인데, 중복된 모든 행을 원본까지 포함해서 전부 삭제합니다.

예를 들어, "김철수"가 두 번 나오면 둘 다 지워버리는 거죠. "중복이 있는 데이터는 신뢰할 수 없으니 아예 분석에서 제외하겠다"는 엄격한 정책을 적용할 때 사용합니다.

하지만 데이터 손실이 크므로 신중하게 사용해야 합니다. 여러분이 이 코드를 사용하면 데이터 품질을 크게 향상시킬 수 있습니다.

실무에서는 제거 전후의 행 개수를 비교하여 "원본 10,000개 중 500개의 중복을 제거하여 9,500개로 정리했습니다"라고 보고하고, 어떤 기준으로 제거했는지 문서화합니다. 또한 제거된 데이터를 별도로 저장해두면, 나중에 문제가 생겼을 때 검토할 수 있어서 안전합니다.

실전 팁

💡 중복 제거 전에 반드시 df_backup = df.copy()로 백업을 만드세요. 잘못 제거했을 때 되돌릴 수 있습니다.

💡 inplace=True 옵션은 원본을 직접 수정하므로 위험합니다. df_clean = df.drop_duplicates()처럼 새 변수에 저장하는 것이 더 안전합니다.

💡 중복 제거 후 df.reset_index(drop=True)로 인덱스를 재정렬하면 0, 1, 2, 3... 식으로 깔끔하게 정리됩니다.

💡 여러 컬럼 조합으로 중복을 판단하고 싶다면 subset=['email', 'phone']처럼 리스트에 여러 컬럼을 넣으세요. 이메일과 전화번호가 모두 같아야 중복으로 인식합니다.

💡 제거된 중복 데이터를 확인하고 싶다면, 제거 전에 removed = df[df.duplicated(subset=['name'], keep='first')]로 제거될 행들을 추출한 후 removed.to_csv('removed_duplicates.csv')로 저장하세요.


6. 데이터_타입_확인과_변환

시작하며

여러분이 CSV 파일을 읽어왔는데, 숫자여야 할 나이가 문자열로 저장되어 있거나, 날짜가 텍스트로 되어 있는 경우를 본 적 있나요? 이런 상황에서 평균을 계산하려고 하면 에러가 발생합니다.

이런 문제는 데이터 분석의 초반에 꼭 마주치는 장애물입니다. 파일에서 읽어온 데이터는 종종 잘못된 타입으로 인식되는데, 예를 들어 "25"라는 나이가 숫자 25가 아니라 문자열 "25"로 저장되면 수학 계산이 불가능합니다.

또한 "2024-01-01"이라는 날짜가 문자열로만 있으면 날짜 차이 계산이나 월별 집계 같은 시계열 분석을 할 수 없죠. 바로 이럴 때 필요한 것이 데이터 타입 확인과 변환입니다.

각 컬럼이 어떤 타입으로 저장되어 있는지 파악하고, 올바른 타입으로 변환해야 정확한 분석과 계산이 가능합니다.

개요

간단히 말해서, 데이터 타입 확인은 각 컬럼이 숫자, 문자열, 날짜 등 어떤 형식으로 저장되어 있는지 알아보는 것이고, 변환은 그것을 올바른 형식으로 바꾸는 작업입니다. pandas에서는 주로 다섯 가지 타입이 사용됩니다: int64(정수), float64(실수), object(문자열), datetime64(날짜/시간), bool(참/거짓).

실무에서 CSV를 읽으면 숫자 컬럼이 object로 잘못 인식되는 경우가 매우 흔합니다. 예를 들어, 가격 컬럼에 "1,000원"처럼 쉼표나 단위가 섞여 있으면 pandas가 숫자로 인식하지 못하고 문자열로 읽습니다.

이를 정리하고 int나 float로 변환해야 합계나 평균을 구할 수 있습니다. 기존에는 타입 오류가 발생하면 그때그때 수동으로 고쳤다면, 이제는 dtypes, astype(), to_numeric(), to_datetime() 같은 메서드로 체계적으로 타입을 관리할 수 있습니다.

핵심 특징은 첫째, 현재 데이터 타입을 한눈에 파악할 수 있고, 둘째, 타입 변환 시 에러 처리 옵션(에러 무시, NaN 변환 등)을 설정할 수 있으며, 셋째, 여러 컬럼을 한 번에 변환할 수도 있다는 점입니다. 이러한 기능들이 대용량 데이터에서도 안전하고 빠르게 타입을 정리할 수 있게 해줍니다.

코드 예제

import pandas as pd
import numpy as np

# 샘플 데이터 (타입이 섞여 있음)
df = pd.DataFrame({
    'name': ['김철수', '이영희', '박민수', '최지영'],
    'age': ['25', '30', 'N/A', '28'],  # 문자열로 저장됨
    'price': ['1000', '2000', '1500', 'invalid'],  # 문자열 + 잘못된 값
    'signup_date': ['2024-01-01', '2024-02-15', '2024-03-20', '2024-04-10'],  # 문자열
    'is_active': ['True', 'False', 'True', 'True']  # 문자열
})

print("원본 데이터:")
print(df)
print("\n원본 데이터 타입:")
print(df.dtypes)

# 방법 1: age를 숫자로 변환 (에러 발생 시 NaN 처리)
df['age'] = pd.to_numeric(df['age'], errors='coerce')
print("\nage 변환 후:")
print(df['age'])

# 방법 2: price를 정수로 변환 (에러 발생 시 NaN 처리)
df['price'] = pd.to_numeric(df['price'], errors='coerce').astype('Int64')
print("\nprice 변환 후:")
print(df['price'])

# 방법 3: signup_date를 datetime으로 변환
df['signup_date'] = pd.to_datetime(df['signup_date'])
print("\nsignup_date 변환 후:")
print(df['signup_date'])

# 방법 4: is_active를 boolean으로 변환
df['is_active'] = df['is_active'].map({'True': True, 'False': False})
print("\nis_active 변환 후:")
print(df['is_active'])

print("\n변환 후 데이터 타입:")
print(df.dtypes)

설명

이 코드가 하는 일은 잘못된 타입으로 저장된 데이터를 분석에 적합한 올바른 타입으로 변환하는 것입니다. 첫 번째 단계에서 df.dtypes로 현재 타입을 확인하면, 모든 컬럼이 'object'(문자열)로 되어 있는 것을 볼 수 있습니다.

이는 CSV에서 읽어올 때 pandas가 안전하게 모든 것을 문자열로 간주했기 때문입니다. 하지만 이 상태로는 나이의 평균을 계산하거나 가격의 합계를 구할 수 없습니다.

두 번째 단계에서 pd.to_numeric(df['age'], errors='coerce')는 age 컬럼을 숫자로 변환하려고 시도합니다. errors='coerce' 옵션은 "변환할 수 없는 값(예: 'N/A')은 NaN으로 바꿔라"는 뜻입니다.

만약 이 옵션 없이 변환하면 'N/A' 때문에 에러가 발생하여 전체 변환이 실패하게 됩니다. 이렇게 하면 '25', '30', '28'은 숫자 25, 30, 28로 바뀌고, 'N/A'는 NaN이 되어 나중에 결측치 처리를 할 수 있습니다.

세 번째 단계의 price 변환도 비슷한데, astype('Int64')를 추가로 사용하여 정수 타입으로 명확히 지정합니다. 여기서 'Int64'(대문자 I)는 pandas의 nullable integer 타입으로, NaN을 허용하는 정수입니다.

일반 'int64'(소문자 i)는 NaN을 허용하지 않아서 변환 시 에러가 발생할 수 있으니 주의하세요. 네 번째 단계에서 pd.to_datetime()은 날짜 문자열을 datetime 타입으로 변환합니다.

이렇게 하면 날짜 차이 계산(df['signup_date'] - df['other_date']), 월별 집계(df.groupby(df['signup_date'].dt.month)), 날짜 정렬 등 다양한 시계열 분석이 가능해집니다. pandas는 대부분의 표준 날짜 형식을 자동으로 인식하지만, 특이한 형식이라면 format='%Y/%m/%d' 같은 옵션을 추가해야 합니다.

다섯 번째 단계의 boolean 변환은 .map() 함수로 문자열 'True'/'False'를 실제 Python의 True/False로 바꿉니다. 이렇게 하면 조건 필터링(df[df['is_active']])이나 통계(df['is_active'].sum())가 자연스럽게 작동합니다.

여러분이 이 코드를 사용하면 데이터를 분석 가능한 상태로 만들 수 있습니다. 실무에서는 데이터를 읽어온 직후 가장 먼저 타입을 확인하고 변환하는 것이 표준 절차입니다.

타입이 올바르지 않으면 나중에 집계나 시각화 단계에서 예상치 못한 에러가 발생하여 시간을 낭비하게 됩니다.

실전 팁

💡 errors='coerce' 대신 errors='raise'를 사용하면 변환 실패 시 에러를 발생시켜 어떤 값이 문제인지 정확히 파악할 수 있습니다. 디버깅할 때 유용해요.

💡 CSV 읽을 때부터 타입을 지정하고 싶다면 pd.read_csv('data.csv', dtype={'age': int, 'price': float})처럼 dtype 옵션을 사용하세요. 나중에 변환하는 것보다 빠릅니다.

💡 날짜 형식이 특이하다면 pd.to_datetime(df['date'], format='%d/%m/%Y')처럼 format을 명시하세요. 예를 들어, '31/12/2024'는 일/월/년 순서니까 %d/%m/%Y입니다.

💡 여러 컬럼을 한 번에 변환하고 싶다면 df[['age', 'price']] = df[['age', 'price']].apply(pd.to_numeric, errors='coerce')처럼 apply를 활용하세요.

💡 타입 변환 후 df.info()를 다시 실행하여 모든 컬럼이 의도한 타입으로 바뀌었는지 확인하세요. 놓친 컬럼이 있을 수 있습니다.


7. 이상치_탐지하기

시작하며

여러분이 직원들의 급여 데이터를 분석하는데, 평균이 300만 원인데 한 사람의 급여가 1억 원으로 기록되어 있다면 어떻게 하시겠어요? "이게 진짜 데이터일까, 입력 오류일까?" 하는 의문이 들 겁니다.

이런 극단적인 값을 이상치(Outlier)라고 부르며, 실무에서 매우 흔하게 발생합니다. 센서가 고장나서 터무니없는 값을 기록하거나, 사람이 0을 하나 더 입력하거나, 단위를 잘못 이해해서(킬로그램을 그램으로 입력) 생깁니다.

이상치를 그대로 두면 평균, 표준편차 같은 통계값이 크게 왜곡되어 잘못된 결론을 내리게 됩니다. 바로 이럴 때 필요한 것이 이상치 탐지입니다.

정상 범위를 벗어난 값을 찾아내야, 제거하거나 수정하거나 별도로 분석할 수 있습니다.

개요

간단히 말해서, 이상치 탐지는 데이터 분포에서 다른 값들과 크게 동떨어진 극단적인 값을 찾아내는 작업입니다. 실무에서 가장 많이 사용하는 방법은 IQR(Interquartile Range, 사분위수 범위)과 Z-score(표준점수)입니다.

IQR 방법은 데이터를 4등분했을 때 중간 50%의 범위를 구하고, 그 범위의 1.5배를 벗어나면 이상치로 간주합니다. 예를 들어, 시험 점수가 25점~75점 사이에 대부분 분포하는데, 5점이나 100점이 있다면 이상치일 가능성이 높습니다.

Z-score 방법은 평균에서 표준편차의 3배 이상 떨어진 값을 이상치로 봅니다. 전통적으로는 데이터를 정렬하여 눈으로 확인하거나 히스토그램을 그려서 찾았다면, 이제는 pandas와 numpy를 사용하여 수학적 기준으로 자동으로 이상치를 탐지할 수 있습니다.

핵심 특징은 첫째, 객관적이고 재현 가능한 기준으로 이상치를 판단할 수 있고, 둘째, 수천 개의 데이터에서도 즉시 이상치를 찾아낼 수 있으며, 셋째, 시각화와 결합하여 직관적으로 확인할 수도 있다는 점입니다. 이러한 방법들이 데이터 품질 관리의 핵심 도구가 됩니다.

코드 예제

import pandas as pd
import numpy as np

# 샘플 데이터 (이상치 포함)
df = pd.DataFrame({
    'name': ['김철수', '이영희', '박민수', '최지영', '정다은', '홍길동', '이순신'],
    'age': [25, 30, 28, 150, 32, 27, -5],  # 150과 -5가 이상치
    'salary': [3000, 3200, 3500, 3100, 100000, 2900, 3300]  # 100000이 이상치
})

print("원본 데이터:")
print(df)

# 방법 1: IQR 방법으로 이상치 탐지
def detect_outliers_iqr(data, column):
    Q1 = data[column].quantile(0.25)
    Q3 = data[column].quantile(0.75)
    IQR = Q3 - Q1
    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR
    outliers = data[(data[column] < lower_bound) | (data[column] > upper_bound)]
    return outliers, lower_bound, upper_bound

age_outliers, age_lower, age_upper = detect_outliers_iqr(df, 'age')
print(f"\n[IQR 방법] age 이상치 범위: {age_lower:.2f} ~ {age_upper:.2f}")
print("age 이상치:")
print(age_outliers[['name', 'age']])

salary_outliers, salary_lower, salary_upper = detect_outliers_iqr(df, 'salary')
print(f"\n[IQR 방법] salary 이상치 범위: {salary_lower:.2f} ~ {salary_upper:.2f}")
print("salary 이상치:")
print(salary_outliers[['name', 'salary']])

# 방법 2: Z-score 방법으로 이상치 탐지
def detect_outliers_zscore(data, column, threshold=3):
    mean = data[column].mean()
    std = data[column].std()
    z_scores = np.abs((data[column] - mean) / std)
    outliers = data[z_scores > threshold]
    return outliers

age_outliers_z = detect_outliers_zscore(df, 'age')
print("\n[Z-score 방법] age 이상치:")
print(age_outliers_z[['name', 'age']])

설명

이 코드가 하는 일은 통계적 기준을 사용하여 정상 범위를 크게 벗어난 극단적인 값들을 자동으로 찾아내는 것입니다. 첫 번째 방법인 IQR(사분위수 범위)은 데이터를 크기 순으로 정렬했을 때 25번째 백분위수(Q1)와 75번째 백분위수(Q3)의 차이를 구합니다.

예를 들어, 나이 데이터가 25, 27, 28, 30, 32라면 Q1은 27, Q3는 30이므로 IQR은 3입니다. 그리고 하한선은 Q1 - 1.5 * IQR = 27 - 4.5 = 22.5, 상한선은 Q3 + 1.5 * IQR = 30 + 4.5 = 34.5가 됩니다.

즉, 22.5보다 작거나 34.5보다 큰 값은 이상치로 판단하는 거죠. 150이나 -5는 확실히 이 범위를 벗어나므로 이상치로 탐지됩니다.

이 방법의 장점은 데이터의 분포에 민감하지 않고, 중간 50%의 데이터만 기준으로 삼기 때문에 극단값에 영향을 받지 않는다는 점입니다. 박스플롯(Box Plot)을 그렸을 때 수염(whisker) 밖에 있는 점들이 바로 IQR 기준의 이상치입니다.

두 번째 방법인 Z-score는 각 값이 평균에서 표준편차의 몇 배 떨어져 있는지 계산합니다. 공식은 (값 - 평균) / 표준편차입니다.

예를 들어, 급여 평균이 3,000만 원이고 표준편차가 200만 원인데, 어떤 사람의 급여가 3,600만 원이라면 Z-score는 (3600 - 3000) / 200 = 3입니다. 일반적으로 Z-score의 절댓값이 3을 넘으면 이상치로 간주합니다.

정규분포에서 평균에서 ±3 표준편차 밖의 데이터는 전체의 0.3%밖에 안 되기 때문에, 매우 드문 값이라고 볼 수 있습니다. 코드에서 np.abs()를 사용하여 절댓값을 구하는 이유는, 평균보다 크게 큰 값(+)과 크게 작은 값(-) 모두를 이상치로 찾기 위해서입니다.

100,000이라는 급여는 평균에서 엄청나게 멀기 때문에 Z-score가 매우 크게 나와서 이상치로 탐지됩니다. 여러분이 이 코드를 사용하면 주관적인 판단 없이 객관적인 기준으로 이상치를 찾을 수 있습니다.

실무에서는 이상치를 찾은 후, "이것이 정말 오류인가, 아니면 특별한 의미가 있는 데이터인가"를 도메인 지식을 바탕으로 판단합니다. 예를 들어, 급여 100,000이 CEO의 연봉이라면 오류가 아니라 정상 데이터일 수 있으니, 별도의 VIP 그룹으로 분류하는 것이 맞습니다.

실전 팁

💡 IQR 방법의 1.5배는 표준 기준인데, 더 엄격하게 하려면 1.0배로, 더 느슨하게 하려면 2.0배로 조정할 수 있습니다. 데이터의 특성에 따라 조절하세요.

💡 Z-score는 데이터가 정규분포를 따를 때 가장 효과적입니다. 분포가 심하게 왜곡되어 있다면 IQR 방법이 더 안전합니다.

💡 이상치를 찾았다고 무조건 삭제하지 마세요. 먼저 outliers.to_csv('outliers_review.csv')로 저장하여 관계자와 검토한 후 조치를 결정하세요.

💡 여러 컬럼에 대해 이상치를 한 번에 검사하고 싶다면 for col in ['age', 'salary', 'score']: 같은 반복문을 사용하세요.

💡 시각화를 추가하면 더 직관적입니다. df.boxplot(column='age')로 박스플롯을 그리면 이상치가 점으로 표시되어 한눈에 볼 수 있습니다.


8. 데이터_분포_확인하기

시작하며

여러분이 고객의 나이 데이터를 분석하는데, 평균만 보고 "우리 고객의 평균 나이는 35세입니다"라고 보고하면 충분할까요? 실제로는 10대부터 80대까지 고르게 분포되어 있을 수도 있고, 대부분이 30대에 집중되어 있을 수도 있습니다.

이런 차이는 마케팅 전략이나 상품 기획에 큰 영향을 미칩니다. 평균만 보면 놓치는 정보가 너무 많습니다.

데이터가 어떻게 퍼져 있는지, 특정 구간에 몰려 있는지, 양쪽 끝에 극단값이 있는지 등을 알아야 데이터의 전체 그림을 이해할 수 있습니다. 바로 이럴 때 필요한 것이 데이터 분포 확인입니다.

평균, 중앙값, 표준편차 같은 기술통계량과 히스토그램을 통해 데이터의 전체적인 모습을 파악하는 것이죠.

개요

간단히 말해서, 데이터 분포 확인은 데이터가 어떤 값들을 중심으로 어떻게 흩어져 있는지 통계값과 그래프로 파악하는 작업입니다. 실무에서는 새로운 데이터셋을 받으면 가장 먼저 describe()로 기초 통계를 확인합니다.

평균(mean), 표준편차(std), 최솟값(min), 최댓값(max), 사분위수(25%, 50%, 75%)를 한 번에 볼 수 있어서 데이터의 대략적인 모습을 빠르게 이해할 수 있습니다. 예를 들어, 평균이 100인데 표준편차가 50이면 데이터가 꽤 넓게 퍼져 있다는 뜻이고, 표준편차가 5라면 평균 근처에 모여 있다는 뜻입니다.

전통적으로는 데이터를 일일이 정렬하고 계산기로 평균과 표준편차를 구했다면, 이제는 pandas의 한 줄 코드로 모든 통계량을 자동으로 계산하고, matplotlib로 시각화까지 즉시 할 수 있습니다. 핵심 특징은 첫째, 숫자 컬럼의 모든 통계량을 한 번에 확인할 수 있고, 둘째, 히스토그램으로 분포의 형태(정규분포, 왜곡분포 등)를 시각적으로 파악할 수 있으며, 셋째, 이상치나 데이터 편향을 조기에 발견할 수 있다는 점입니다.

이러한 정보가 데이터 분석의 방향과 전략을 결정하는 데 필수적입니다.

코드 예제

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

# 샘플 데이터
np.random.seed(42)
df = pd.DataFrame({
    'age': np.random.normal(35, 10, 100).astype(int),  # 평균 35, 표준편차 10
    'salary': np.random.normal(3500, 500, 100).astype(int),  # 평균 3500, 표준편차 500
    'score': np.random.uniform(60, 100, 100).astype(int)  # 60~100 균등분포
})

# 기초 통계 확인
print("기초 통계량:")
print(df.describe())

# 각 컬럼별 상세 통계
print("\nage 컬럼 상세 통계:")
print(f"평균: {df['age'].mean():.2f}")
print(f"중앙값: {df['age'].median():.2f}")
print(f"최빈값: {df['age'].mode()[0]}")
print(f"표준편차: {df['age'].std():.2f}")
print(f"분산: {df['age'].var():.2f}")
print(f"왜도(Skewness): {df['age'].skew():.2f}")
print(f"첨도(Kurtosis): {df['age'].kurt():.2f}")

# 히스토그램으로 분포 시각화
plt.figure(figsize=(15, 4))

plt.subplot(1, 3, 1)
plt.hist(df['age'], bins=20, edgecolor='black', alpha=0.7)
plt.title('Age Distribution')
plt.xlabel('Age')
plt.ylabel('Frequency')

plt.subplot(1, 3, 2)
plt.hist(df['salary'], bins=20, edgecolor='black', alpha=0.7, color='green')
plt.title('Salary Distribution')
plt.xlabel('Salary')
plt.ylabel('Frequency')

plt.subplot(1, 3, 3)
plt.hist(df['score'], bins=20, edgecolor='black', alpha=0.7, color='orange')
plt.title('Score Distribution')
plt.xlabel('Score')
plt.ylabel('Frequency')

plt.tight_layout()
plt.savefig('distribution.png')
print("\n히스토그램이 'distribution.png'로 저장되었습니다.")

설명

이 코드가 하는 일은 데이터의 중심 경향, 산포도, 분포 형태를 숫자와 그래프로 종합적으로 분석하는 것입니다. 첫 번째 단계인 df.describe()는 모든 숫자 컬럼에 대해 8가지 통계량을 한 번에 계산합니다.

count(개수), mean(평균), std(표준편차), min(최솟값), 25%(1사분위수), 50%(중앙값), 75%(3사분위수), max(최댓값)입니다. 예를 들어, age의 평균이 35, 중앙값(50%)이 36이라면 분포가 대칭에 가깝다는 뜻입니다.

만약 평균이 35인데 중앙값이 25라면, 오른쪽으로 긴 꼬리(right-skewed)를 가진 분포로, 소수의 고령자가 평균을 끌어올리고 있다는 신호입니다. 두 번째 단계에서 개별 통계량을 더 상세히 계산합니다.

평균(mean)은 모든 값의 합을 개수로 나눈 것이고, 중앙값(median)은 정렬했을 때 정중앙에 있는 값입니다. 최빈값(mode)은 가장 많이 등장하는 값이죠.

표준편차(std)는 데이터가 평균에서 평균적으로 얼마나 떨어져 있는지를 나타내며, 분산(var)은 표준편차의 제곱입니다. 표준편차가 크면 데이터가 넓게 퍼져 있고, 작으면 평균 근처에 모여 있다는 의미입니다.

왜도(Skewness)는 분포의 비대칭 정도를 나타냅니다. 0에 가까우면 대칭(정규분포), 양수면 오른쪽 꼬리가 길고(평균 > 중앙값), 음수면 왼쪽 꼬리가 깁니다(평균 < 중앙값).

예를 들어, 소득 데이터는 대부분 중산층이지만 소수의 고소득자가 있어서 양의 왜도를 보입니다. 첨도(Kurtosis)는 분포의 꼬리가 얼마나 두꺼운지, 즉 극단값이 얼마나 많은지를 나타냅니다.

3에 가까우면 정규분포, 3보다 크면 극단값이 많고, 작으면 적습니다. 세 번째 단계인 히스토그램 시각화는 데이터를 구간(bin)으로 나누어 각 구간에 몇 개의 데이터가 있는지 막대그래프로 보여줍니다.

bins=20은 전체 범위를 20개 구간으로 나눈다는 뜻입니다. 예를 들어, age가 10~70 범위라면 3씩 나누어 10-13, 13-16, ...

같은 구간으로 만들고, 각 구간의 빈도를 막대 높이로 표현합니다. 히스토그램을 보면 데이터가 종 모양(정규분포), U자 모양, 한쪽으로 치우친 모양 등 분포의 형태를 직관적으로 파악할 수 있습니다.

여러분이 이 코드를 사용하면 데이터의 전체적인 특성을 한눈에 이해할 수 있습니다. 실무에서는 보고서에 기초 통계 테이블과 히스토그램을 함께 첨부하여 "우리 고객의 나이는 평균 35세이며, 대부분 25~45세 구간에 분포하고, 정규분포에 가까운 형태입니다"라고 설명합니다.

이런 정보가 타겟 마케팅, 상품 개발, 리스크 관리 등의 의사결정에 직접적으로 활용됩니다.

실전 팁

💡 describe()는 기본적으로 숫자 컬럼만 보여주는데, describe(include='all')을 사용하면 문자열 컬럼까지 포함하여 unique(고유값 개수), top(최빈값), freq(최빈값 빈도)를 볼 수 있습니다.

💡 표준편차가 평균의 30%를 넘으면 데이터가 상당히 불균등하게 분포되어 있다는 신호입니다. 이럴 때는 평균보다 중앙값을 사용하는 것이 더 대표성이 높습니다.

💡 히스토그램의 bins 개수는 데이터 크기에 따라 조절하세요. 데이터가 적으면 bins=10 정도, 많으면 bins=50 이상으로 늘려서 더 세밀하게 볼 수 있습니다.

💡 왜도가 ±1을 넘으면 분포가 심하게 왜곡된 것이니, 로그 변환(np.log(df['salary']))을 고려해보세요. 특히 소득, 가격 같은 데이터는 로그 변환 후 정규분포에 가까워지는 경우가 많습니다.

💡 여러 그룹을 비교하고 싶다면 df.groupby('category')['age'].describe()처럼 그룹별 통계를 확인하거나, 그룹별로 히스토그램을 겹쳐 그려서 차이를 시각적으로 비교하세요.


#Python#Pandas#DataQuality#MissingData#DataCleaning#Data Science

댓글 (0)

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