부트캠프

멋쟁이사자처럼 부트캠프 그로스마케팅 4기 24일차_260413

Yuuma 2026. 4. 13. 17:27

 

[TIL] Day8 | 결측치(Missing Value)와 이상치(Outlier)
처리로 분석의 신뢰도 높이기


💡 오늘 한 줄 요약 데이터를 믿고 바로 계산하는 게 마케터가 가장 많이 하는 실수. 결측치·이상치를 먼저 처리하고 세그먼트별로 쪼개야 진짜 인사이트가 보인다.

1. Pandas 더 나아가기

라이브러리란?

라이브러리 = 누군가 미리 만들어 놓은 기능 묶음. 파이썬에 기본으로 없는 기능을 가져다 쓸 수 있습니다. import 라이브러리이름 as 별명 형태로 불러옵니다.

라이브러리 하는 일 불러오는 코드
pandas 표 형태 데이터 처리 (핵심) import pandas as pd
matplotlib 그래프 그리기 import matplotlib.pyplot as plt

1-1. 판다스 시작하기 — NaN이란?

📌 NaN = "Not a Number"
판다스에서 빈 값을 표현하는 방식입니다. None을 넣으면 판다스가 자동으로 NaN으로 바꿉니다.
import pandas as pd

data = {
    'name': ['Alice', 'Bob', 'Carol', ...],
    'service': ['멜론', '스포티파이', None, ...],   # None → NaN으로 자동 변환
    'likes': [10, 5, None, 8, ...],
    'income': [3200, 4800, None, ...],
}
df = pd.DataFrame(data)

2. 데이터 첫 탐색 3종 세트 ⭐⭐⭐

🚨 마케터가 데이터 분석에서 가장 많이 하는 실수
"데이터를 믿고 바로 계산하는 것." Age 컬럼에 999가 있으면 평균 나이가 터무니없이 높아지고, Income에 24개의 결측치가 있으면 평균 소득 계산이 틀립니다. 데이터를 받으면 무조건 아래 세 줄 먼저 실행하세요.
함수 하는 일
df.info() 컬럼 이름, 타입, 결측치 수를 한번에
df.describe() 숫자 컬럼의 평균·최솟값·최댓값 등 기초 통계
df.head(n) 처음 n행 미리보기 (기본값 5)
df.info()       # 컬럼 타입 + 결측치 수
df.describe()   # 숫자 컬럼 기초 통계
df.head()       # 처음 5행 미리보기

info() 결과 읽는 법

RangeIndex: 2240 entries          ← 총 2240행
 #  Column       Non-Null Count  Dtype
--- ------       --------------  -----
 4  Income       2216 non-null   float64  ← 결측치 24개!
 7  Dt_Customer  2240 non-null   object   ← 날짜인데 문자열 → 변환 필요
핵심 읽는 포인트
Non-Null Count가 전체 행 수보다 작으면 → 결측치 있음
Dtypeobject인데 날짜·숫자여야 할 컬럼이면 → 타입 변환 필요

3. 데이터 선택: loc vs iloc ⭐⭐⭐

판다스에서 데이터를 '꺼내는' 방법 두 가지입니다.

  loc iloc
기준 이름(라벨) · 조건 숫자 순서 (0부터)
끝 포함? 포함 미포함
주로 쓸 때 조건 필터링, 값 수정 몇 번째 행인지 알 때

loc — 이름과 조건으로 선택 (마케터가 더 많이 씁니다)

# 조건으로 행 필터링 ← 실무에서 가장 자주 씁니다
df.loc[df['service'] == '멜론']         # 멜론 사용자만
df.loc[df['age'] >= 25]                  # 25살 이상

# AND 조건 (&) — 각각 괄호()로 감싸야 합니다!
df.loc[(df['age'] >= 25) & (df['service'] == '멜론')]

# OR 조건 (|)
df.loc[(df['service'] == '멜론') | (df['service'] == '스포티파이')]

# 값 수정 — 조건에 맞는 행의 특정 컬럼 값을 바꿀 때
df.loc[df['satisfaction'] >= 8, 'grade'] = 'VIP'
df.loc[df['satisfaction'] < 5, 'grade'] = '이탈위험'
실무에서 loc를 쓰는 상황
"캠페인에 반응한 고객만 뽑아줘" → 조건 필터링 → loc
"이탈 고객 중 30대만 보고 싶어" → AND 조건 → loc
"만족도 8점 이상 고객에게 VIP 태그 달기" → 값 수정 → loc

iloc — 순서(번호)로 선택

df.iloc[0]          # 첫 번째 행 → Series (세로로 눕혀짐)
df.iloc[[0]]        # 첫 번째 행 → DataFrame 유지
df.iloc[-1]         # 마지막 행
df.iloc[0:5]        # 0,1,2,3,4번 행 (끝 미포함!)
df.iloc[-3:]        # 마지막 3행
Series vs DataFrame 헷갈릴 때
결과가 여러 행 → DataFrame (표)  |  결과가 한 행 or 한 열 → Series (목록)
행 하나를 뽑는 순간 표가 목록이 됩니다. 표 형태를 유지하려면 df.iloc[[n]]처럼 이중 괄호를 쓰세요.

4. 결측치 (Missing Value) ⭐⭐⭐⭐

🚨 왜 결측치를 처리해야 하나요?
결측치를 그냥 두면 판다스는 그 행을 조용히 계산에서 제외합니다. "내가 모르는 사이에 일부 고객이 분석에서 빠져있는" 상황이 됩니다.

예: Income이 비어있는 24명 → 평균 소득 계산 시 2240명이 아닌 2216명 기준. 그 24명이 특정 학력군에 몰려있다면 학력별 소득 분석 결과 자체가 왜곡됩니다.
# 결측치 현황 파악
df.isnull().sum()           # 컬럼별 결측치 개수
df.isnull().mean() * 100    # 결측치 비율(%)

처리 방법 4가지

결측치 처리 방법 4가지 비교

결측치 처리 방법 비교 — 방법 3, 4가 실무에서 자주 쓰입니다

# 방법 1: 행 제거 — 결측 비율이 낮을 때
df = df.dropna(subset=['likes'])

# 방법 2: 문자열 → '미응답'으로 채우기
df['service'] = df['service'].fillna('미응답')
# "어떤 서비스를 쓰는지 모른다"는 것 자체가 정보! 삭제 X

# 방법 3: 숫자 → 중앙값으로 채우기 ← 실무에서 가장 자주 씁니다
df['income'] = df['income'].fillna(df['income'].median())

# 방법 4: 그룹별 중앙값으로 채우기 ← 가장 정교한 방법
df['income'] = df['income'].fillna(
    df.groupby('service')['income'].transform('median')
)
# Carol(애플뮤직) → 애플뮤직 사용자 income 중앙값으로 채움
💡 왜 평균이 아닌 중앙값?
소득·구매금액처럼 마케팅 데이터는 한쪽으로 치우친 경우가 많습니다. 100명 중 한 명이 666,666달러라면 평균은 그 한 명에 끌려 올라가지만, 중앙값은 순서상 가운데 값이라 극단값 영향을 받지 않습니다.

💡 왜 그룹별 중앙값이 더 좋은가?
PhD 학력자 중앙값 ≈ 65,000달러 vs Basic 학력자 중앙값 ≈ 20,000달러. 전체 중앙값(≈51,000달러)으로 채우면 각 그룹의 특성이 무너집니다.
⚠️ 판다스 함수는 원본을 건드리지 않습니다
원본에 적용하려면 반드시 df = df.dropna() 또는 inplace=True를 써야 합니다.

5. 이상치 (Outlier) ⭐⭐⭐⭐

이상치 = 다른 값들과 동떨어진 비정상적인 값. 예: 나이 999, 소득 666,666

🚨 왜 이상치를 처리해야 하나요?
이상치는 평균을 왜곡시킵니다. 고객 30명 평균 구매금액 계산인데 한 명이 테스트 주문으로 100만 원을 결제했다면, 그 한 건이 전체 평균을 끌어올려 실제와 동떨어진 의사결정으로 이어집니다.
IQR 이상치 탐지 원리

IQR = 데이터 중간 50% 범위. 박스플롯에서 수염 끝이 바로 이 기준입니다.

# 기초 통계로 먼저 확인 — max가 말도 안 되게 크면 이상치!
df['age'].describe()

# IQR 방식으로 이상치 기준 계산
Q1 = df['age'].quantile(0.25)   # 하위 25%
Q3 = df['age'].quantile(0.75)   # 상위 75%
IQR = Q3 - Q1
upper = Q3 + 1.5 * IQR            # 이 값보다 크면 이상치

# 이상치 확인 후 제거 — .copy()로 원본과 독립적인 DataFrame 생성
df_clean = df[df['age'] <= upper].copy()
이상치 처리 판단 기준
✅ 나이 999 → 입력 오류 → 제거
✅ 소득 666,666 → 오입력 가능성 → 제거
⚠️ 구매금액이 유독 높은 VIP → 진짜 데이터 → 유지하되 별도 세그먼트로 분석

이상치가 실제 고객 행동인지 데이터 오류인지를 먼저 판단하는 게 중요합니다.

6. 파생변수 만들기 ⭐⭐⭐

기존 컬럼을 조합해서 새로운 지표를 만드는 것. 마케터에게 파생변수란 원재료를 실제로 쓸 수 있는 지표로 바꾸는 작업입니다.

파생변수가 필요한 이유
출생연도 → 나이 → 연령대 → "30대 타겟" 캠페인 필터
카테고리별 구매금액 → 총 구매금액 → 고가치 고객 식별
만족도 점수 → VIP / 이탈위험 → 액션별 커뮤니케이션 설계
# 방법 1: 계산으로 새 컬럼
df['income_만원'] = df['income'] / 10000

# 방법 2: 조건으로 새 컬럼 (loc 사용)
df['grade'] = '일반'
df.loc[df['satisfaction'] >= 8, 'grade'] = 'VIP'
df.loc[df['satisfaction'] <= 3, 'grade'] = '이탈위험'

# 방법 3: pd.cut — 숫자를 구간(범주)으로 나누기
df['age_group'] = pd.cut(
    df['age'],
    bins=[0, 29, 39, 49, 100],
    labels=['20대', '30대', '40대', '50대+']
)

# 방법 4: 열 방향 합산 (axis=1) — 한 행 안에서 여러 컬럼을 더할 때
df['engagement'] = df[['likes', 'satisfaction']].sum(axis=1)

# 방법 5: True/False 활용
df['is_high_income'] = df['income'] >= 5000
⚠️ 파생변수는 반드시 이상치 제거 후에 만드세요!
나이 999가 있는 상태에서 pd.cut을 돌리면 999가 '50대+' 그룹에 들어가 버립니다.

7. groupby: 그룹별 집계 ⭐⭐⭐⭐

SQL의 GROUP BY와 같습니다. 마케터가 하는 분석 대부분은 "전체 평균"이 아니라 "세그먼트별 차이"를 보는 것입니다.

groupby가 마케터에게 중요한 이유

전체 이탈률 31%만 봐서는 아무것도 할 수 없습니다. 연령대별로 쪼개야 액션이 나옵니다.

함수 하는 일
df.groupby('col')['target'].mean() 그룹별 평균
df.groupby('col')['target'].count() 그룹별 행 개수
df.groupby('col').agg({...}) 그룹별 여러 통계 한번에
.sort_values(ascending=False) 내림차순 정렬
.round(1) 소수점 1자리로 반올림
# 서비스별 평균 만족도
df.groupby('service')['satisfaction'].mean()

# 서비스별 여러 통계 한번에
df.groupby('service').agg({
    'satisfaction': 'mean',
    'income': 'mean',
    'name': 'count'
})

# 정렬 + 반올림
df.groupby('service')['satisfaction'].mean().round(1).sort_values(ascending=False)
groupby가 그로스 마케팅의 핵심인 이유
전체 캠페인 반응율 15% → 학력별로 보면 PhD는 22%, Basic은 7%
전체 이탈율 31% → 연령대별로 보면 20대는 45%, 50대는 18%

이 차이를 발견하는 것이 그로스 마케팅의 핵심입니다. 세그먼트별로 다른 메시지, 다른 채널, 다른 혜택을 설계하기 위해서입니다.

8. 실습 1 — music_users.csv 전처리

4단계로 전처리 파이프라인을 적용합니다.

데이터 전처리 파이프라인

전처리 순서: 결측치 처리 → 이상치 제거 → 파생변수 생성 → 분석

Step 1. 결측치 확인 및 처리

# 현황 파악
print(users.isnull().sum())

# 범주형: '미응답'으로 채우기
users['service'] = users['service'].fillna('미응답')

# 숫자형: 중앙값으로 채우기
users['age'] = users['age'].fillna(users['age'].median())
users['listen_hours'] = users['listen_hours'].fillna(users['listen_hours'].median())
users['satisfaction'] = users['satisfaction'].fillna(users['satisfaction'].median())
users['monthly_fee'] = users['monthly_fee'].fillna(users['monthly_fee'].median())

print(users.isnull().sum())   # 모두 0이면 완료

Step 2. 이상치 처리

# max: 999.0 → 이상치!
print(users['age'].describe())

# IQR로 상한선 자동 계산 — 사람이 임의로 정하면 틀릴 수 있으니 데이터 스스로 정하게
Q1 = users['age'].quantile(0.25)   # → 27.5
Q3 = users['age'].quantile(0.75)   # → 35.5
upper = Q3 + 1.5 * (Q3 - Q1)       # → 47.5

# .copy() — 필터링한 데이터만 복사해서 원본과 독립
users_clean = users[users['age'] <= upper].copy()

Step 3. 파생변수 생성

# 연령대 — 이상치 제거 후 pd.cut 실행!
users_clean['age_group'] = pd.cut(
    users_clean['age'],
    bins=[0, 29, 39, 49, 100],
    labels=['20대', '30대', '40대', '50대+']
)

# 고객 등급 — 만족도 기준
users_clean['grade'] = '일반'
users_clean.loc[users_clean['satisfaction'] >= 8, 'grade'] = 'VIP'
users_clean.loc[users_clean['satisfaction'] <= 3, 'grade'] = '이탈위험'

Step 4. 분석

# 서비스별 평균 만족도
users_clean.groupby('service')['satisfaction'].mean().round(2)

# 전체 이탈율
churn_rate = (users_clean['churn'] == 'Yes').mean() * 100
print(f'전체 이탈율: {churn_rate:.1f}%')

# 등급별 인원 수
users_clean['grade'].value_counts()

# 나이대별 평균 청취 시간
users_clean.groupby('age_group', observed=True)['listen_hours'].mean().round(1)
💡 FutureWarning 해결
age_group이 categorical 타입일 때 groupby()observed=True를 추가하면 경고가 사라집니다. 실제 있는 데이터만 보고 싶을 때 씁니다.

📎 치트시트

함수 하는 일
df.info() 컬럼 타입 + 결측치 수
df.describe() 숫자 컬럼 기초 통계
df.isnull().sum() 결측치 개수
df['col'].fillna(값) 결측치 채우기
df.dropna(subset=['col']) 결측 행 제거
df['col'].quantile(0.75) 상위 75% 값 (Q3)
df[조건].copy() 조건 필터링 후 복사
df.loc[조건, 'col'] = 값 조건부 값 수정
pd.cut(col, bins, labels) 숫자를 구간으로 나누기
df[cols].sum(axis=1) 열 방향 합산
df.groupby('col').agg({}) 그룹별 집계
df['col'].value_counts() 값별 빈도
df.nlargest(n, 'col') 값이 큰 상위 n행

📌 오늘의 핵심 정리

  1. 데이터를 받으면 무조건 3종 세트 먼저info(), describe(), head()로 결측치·타입·이상치를 확인하고 분석을 시작하세요.
  2. 결측치는 조용히 분석을 왜곡합니다 — 숫자형은 그룹별 중앙값, 범주형은 '미응답'으로 채우는 게 가장 정교한 방법입니다.
  3. 이상치는 IQR로 기준을 데이터가 스스로 정하게 — 사람이 임의로 "50 넘으면 이상치"라고 정하면 틀릴 수 있습니다. 단, 이상치 = 오류인지, 진짜 VIP인지 먼저 판단하세요.
  4. 파생변수는 이상치 제거 후에 — 나이 999가 있는 상태에서 pd.cut을 돌리면 999가 '50대+' 그룹에 들어가 버립니다.
  5. groupby가 그로스 마케팅의 핵심 — "전체 이탈률 31%"는 아무것도 할 수 없습니다. 연령대별, 서비스별로 쪼개야 세그먼트별 다른 전략이 나옵니다.

📌 다음 수업: YouTube API로 댓글 데이터 수집 및 텍스트 분석