데이터 수집 및 전처리
전자상거래 퍼널 분석
실제 이커머스 데이터로 배우는 퍼널 분석의 모든 것.
"왜 고객이 이탈하는가?"에 대한 답을 데이터로 찾아가는 여정.
1전자상거래 이벤트 데이터의 특성
일반 이벤트 vs 전자상거래 이벤트
이전 수업에서 다룬 일반 이벤트 데이터는 "누가, 무엇을, 언제"라는 기본 구조였습니다. 전자상거래 이벤트 데이터는 여기에 "구매 여정의 단계"라는 개념이 추가됩니다. 각 이벤트는 독립적으로 존재하는 것이 아니라, 구매라는 목표를 향한 흐름 위에 존재합니다.
# 일반 이벤트: 각 이벤트가 독립적으로 존재
user_id | event | timestamp
--------|-----------|----------
101 | page_view | 09:00
101 | scroll | 09:01
101 | click | 09:02
# 전자상거래 이벤트: 구매 흐름 위에 존재
user_id | event | timestamp | item_id | 퍼널 단계
--------|-------------|-----------|---------|----------
101 | view | 09:00 | A001 | 1단계: 탐색
101 | addtocart | 09:03 | A001 | 2단계: 의향
101 | transaction | 09:08 | A001 | 3단계: 전환GA4 표준 퍼널 계층 구조
전자상거래 이벤트는 단순히 나열되는 것이 아니라 계층 구조를 갖습니다. 각 단계에서 다음 단계로 넘어가지 못한 사용자의 이탈률을 드롭오프율(Drop-off Rate)이라고 합니다.
session_start ← 방문 시작
|
v
view_item ← 상품 상세 조회
|
v
add_to_cart ← 장바구니 담기
|
v
begin_checkout ← 결제 시작
|
v
purchase ← 구매 완료실제 데이터에서 마주치는 현실적인 문제 4가지
| 문제 | 내용 | 예시 |
|---|---|---|
| 비순차적 이벤트 | 사용자가 반드시 순서대로 이동하지 않음 | view → addtocart → view → view → transaction |
| 반복 구매 | 같은 사용자가 퍼널을 여러 번 완주 | 세션 단위로 나눠서 분석 필요 |
| 다중 상품 | 한 거래에 여러 상품 포함 가능 | 이벤트 1건 ≠ 상품 1개 |
| 시간 기반 세션 분리 | GA4의 30분 비활성 기준을 직접 구현 | 세션 분리 로직 직접 코딩 필요 |
2퍼널 분석이란 무엇인가
퍼널(Funnel)은 깔때기를 의미합니다. 많은 사람이 입구로 들어오지만, 최종 목표(구매)에 도달하는 사람은 점점 줄어드는 모습이 깔때기와 닮아있어요. 마케터의 역할은 단순히 각 단계의 숫자를 보는 것이 아니라, 어느 구간에서 가장 많이 이탈하는지를 찾아내는 것입니다.
퍼널 분석 핵심 지표 3가지
| 지표 | 계산식 | 의미 |
|---|---|---|
| 단계별 전환율 | 다음 단계 수 ÷ 현재 단계 수 | 각 단계를 통과하는 비율 |
| 드롭오프율 | 1 − 단계별 전환율 | 각 단계에서 이탈하는 비율 |
| 전체 전환율 | 최종 단계 수 ÷ 첫 단계 수 | 퍼널 전체의 효율 |
마케터의 핵심 업무: 이탈 구간을 찾으면 다음 질문으로 이어집니다.
· 장바구니 → 결제 시작 50% 이탈 → 왜 결제를 시작하지 않았나? (배송비 노출? 로그인 강요?)
· 결제 시작 → 구매 완료 40% 이탈 → 왜 결제를 완료하지 않았나? (UI 문제? 결제 수단 부족?)
이 가설을 데이터로 검증하고 개선하는 것이 그로스 마케터의 핵심 업무입니다.
3실습 데이터셋 : Retailrocket
실제 이커머스 웹사이트에서 4.5개월간 수집된 실제 사용자 행동 데이터입니다. 개인정보 보호를 위해 모든 값이 해시 처리되어 있으며, view / addtocart / transaction 세 가지 이벤트만 존재해 퍼널 분석에 최적화되어 있습니다.
파일별 컬럼 상세
| 파일 | 컬럼명 | 타입 | 설명 |
|---|---|---|---|
| events.csv | timestamp | int | 이벤트 발생시각 (Unix timestamp, 밀리초) |
visitorid | int | 방문자 고유 ID | |
event | string | view / addtocart / transaction | |
itemid | int | 상품 고유 ID | |
transactionid | float | 거래 ID (transaction 이벤트에만 존재, 나머지는 NaN) | |
| item_properties | timestamp | int | 속성 기록 시각 |
itemid | int | 상품 고유 ID | |
property | string | 속성 이름 (categoryid, price, available 등) | |
value | string | 속성 값 | |
| category_tree | categoryid | int | 카테고리 고유 ID |
parentid | float | 상위 카테고리 ID (최상위는 NaN) |
4전처리 실습 : 전자상거래 이벤트 처리
기본 설정 및 파일 불러오기
import pandas as pd
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')
pd.set_option('display.max_columns', None)
pd.set_option('display.float_format', '{:.2f}'.format)
# 이벤트 파일
events = pd.read_csv('events.csv')
# 상품 속성 파일 2개 세로로 이어붙이기
props1 = pd.read_csv('item_properties_part1.csv')
props2 = pd.read_csv('item_properties_part2.csv')
item_props = pd.concat([props1, props2], ignore_index=True)
# 카테고리 트리 파일
category_tree = pd.read_csv('category_tree.csv')events : 2,756,101행 × 5열 item_props : 20,275,902행 × 4열 category_tree: 1,669행 × 2열
print("건수")
print(events['event'].value_counts())
print("비율(%)")
print(events['event'].value_counts(normalize=True) * 100)건수 비율(%) view 2,664,312 96.67 addtocart 69,332 2.52 transaction 22,457 0.81
print("변환 전:", events['timestamp'].iloc[0])
# 출력: 1433221332117 ← 숫자라 날짜 연산 불가!
# Unix 밀리초 → datetime 변환 (unit='ms' : 밀리초 단위임을 명시)
events['timestamp'] = pd.to_datetime(events['timestamp'], unit='ms')
# 날짜 파생 컬럼 생성
events['date'] = events['timestamp'].dt.date
events['hour'] = events['timestamp'].dt.hour
events['day_of_week'] = events['timestamp'].dt.day_name()
events['week'] = events['timestamp'].dt.isocalendar().week.astype(int)
print(f"수집 기간: {(events['timestamp'].max() - events['timestamp'].min()).days}일")변환 전: 1433221332117 수집 기간: 137일
| pd.to_datetime(unit='ms') | Unix 타임스탬프를 날짜/시간 형식으로 변환. unit='ms'는 밀리초 단위를 알려주는 옵션. 없으면 1,000배 커져서 3000년대 날짜가 나옴! |
| .dt.date / .dt.hour | datetime 컬럼에서 날짜·시간·요일·주차를 꺼내는 접근자. 엑셀의 YEAR(), MONTH() 함수와 비슷한 역할 |
| .dt.isocalendar().week | ISO 표준 주차 번호 추출. 주차별 전환율 추이 분석에 사용 |
# 이벤트 종류별로 그룹을 나눠서 결측치 비율 확인
for event_name, group in events.groupby('event'):
null_count = group['transactionid'].isnull().sum()
total_count = len(group)
null_ratio = null_count / total_count * 100
print(f" {event_name}: {null_count}건 ({null_ratio}%)")
# 고유 거래 수 확인 (transaction 이벤트에서)
unique_tx = (
events[events['event'] == 'transaction']
['transactionid'].nunique()
)
print(f"고유 거래 수: {unique_tx:,}건")addtocart : 69,332건 (100.0%) transaction : 0건 (0.0%) ← 이것만 값 있음 view : 2,664,312건 (100.0%) 고유 거래 수: 17,672건
| .groupby('event') | 이벤트 종류별로 데이터를 나누는 함수. for문과 함께 쓰면 각 그룹을 하나씩 꺼내 분석 가능 |
| .isnull().sum() | 결측치(빈 칸) 개수를 세는 함수. isnull()은 빈 칸이면 True, .sum()으로 True의 개수를 합산 |
| .nunique() | 중복 제거 후 고유한 값의 개수를 세는 함수. 같은 거래 ID가 여러 줄이어도 1개로 카운트 |
print(f"처리 전 행 수: {len(events):,}")
# 완전히 동일한 행 확인 (timestamp까지 같아야 진짜 중복)
exact_duplicates = events.duplicated().sum()
print(f"완전 중복 행 수: {exact_duplicates:,}")
events = events.drop_duplicates()
print(f"처리 후 행 수: {len(events):,}")
# 정상적인 반복 이벤트 확인 (같은 사용자가 같은 상품을 여러 번 조회)
repeat_view = (
events[events['event'] == 'view']
.groupby(['visitorid', 'itemid'])
.size()
.reset_index(name='view_count')
)
print(f"동일 상품을 2번 이상 조회한 사용자-상품 쌍: {(repeat_view['view_count'] >= 2).sum():,}건")처리 전 행 수: 2,756,101 완전 중복 행 수: 460 처리 후 행 수: 2,755,641 동일 상품을 2번 이상 조회한 사용자-상품 쌍: 306,548건
핵심 개념: 같은 사용자가 같은 상품을 여러 번 조회하는 것은 오류가 아닙니다. 오히려 구매 의향이 높다는 신호일 수 있어요. timestamp까지 완전히 동일한 행만 진짜 중복으로 처리합니다.
# 분석에 필요한 주요 속성만 추출
key_properties = ['categoryid', 'available']
item_filtered = item_props[item_props['property'].isin(key_properties)].copy()
# 동일 상품+속성에서 가장 최신 값만 유지
item_props_latest = (
item_filtered
.sort_values('timestamp', ascending=False)
.drop_duplicates(subset=['itemid', 'property'])
)
# Long → Wide 변환 (pivot)
item_wide = item_props_latest.pivot(
index='itemid',
columns='property',
values='value'
).reset_index()| .isin(리스트) | 컬럼 값이 리스트 안에 있는 행만 필터링. 엑셀에서 특정 값만 골라내는 VLOOKUP과 비슷한 느낌 |
| .sort_values(ascending=False) | 내림차순 정렬. 가장 최신 timestamp가 위로 올라옴 |
| .pivot(index, columns, values) | 세로(Long) → 가로(Wide) 변환의 핵심 함수. index는 행, columns는 새 컬럼, values는 채울 값 |
# LEFT JOIN: 이벤트는 모두 유지, 상품 속성은 있으면 붙이고 없으면 NaN
events_enriched = events.merge(item_wide, on='itemid', how='left')
print(f"결합 후 행 수: {len(events_enriched):,}")
print(f"컬럼: {list(events_enriched.columns)}")| .merge(how='left') | 두 데이터프레임을 합치는 함수. how='left'는 LEFT JOIN으로, 왼쪽 테이블(events)의 모든 행을 유지하고 오른쪽(item_wide)은 매칭되면 붙임. SQL의 LEFT JOIN과 동일 |
# 방문자별, 시간순으로 정렬
events_enriched = events_enriched.sort_values(
['visitorid', 'timestamp']
).reset_index(drop=True)
# 방문자 내에서 이벤트 순서 번호 부여
events_enriched['event_rank'] = (
events_enriched.groupby('visitorid').cumcount() + 1
)
# 이벤트 약어 매핑: view→V, addtocart→A, transaction→T
event_abbr = {'view': 'V', 'addtocart': 'A', 'transaction': 'T'}
events_enriched['event_abbr'] = events_enriched['event'].map(event_abbr)
# 방문자별 이벤트 시퀀스 문자열 생성
visitor_sequences = (
events_enriched.groupby('visitorid')['event_abbr']
.apply(' → '.join)
.reset_index()
.rename(columns={'event_abbr': 'event_sequence'})
)V 998,984 ← 조회 1번 후 이탈이 가장 많음 V → V 199,117 V → V → V 73,287 V → V → V → V 34,259 V → A 5,457 ← 장바구니까지 간 패턴
| .cumcount() | 그룹 내에서 행 번호를 0부터 부여. +1 하면 1부터 시작. "이 방문자의 첫 번째 이벤트, 두 번째 이벤트..." 순서를 매기는 역할 |
| .map(딕셔너리) | 딕셔너리를 참조해서 값을 변환. 'view'→'V', 'addtocart'→'A' 처럼 일괄 치환 |
| .apply(' → '.join) | 그룹 내 값들을 ' → '로 이어붙여 하나의 문자열로 만듦. V, A, T를 'V → A → T'로 변환 |
5전처리 실습 : 퍼널 분석을 위한 데이터 정제
# 방문자별 발생한 이벤트를 집합(set)으로 묶기
visitor_events = (
events_enriched.groupby('visitorid')['event']
.apply(set) # ['view','view','addtocart'] → {'view','addtocart'}
.reset_index()
.rename(columns={'event': 'event_set'})
)
# 퍼널 단계 분류 함수 정의
def classify_funnel(event_set):
if 'transaction' in event_set: return '3_transaction'
elif 'addtocart' in event_set: return '2_addtocart'
elif 'view' in event_set: return '1_view'
else: return '0_other'
visitor_events['funnel_stage'] = visitor_events['event_set'].apply(classify_funnel)
funnel_counts = visitor_events['funnel_stage'].value_counts().sort_index()| .apply(set) | 각 방문자의 이벤트 목록을 집합(set)으로 변환. view가 100번이어도 {'view'} 하나로 표현됨. 중복 제거 효과 |
| def classify_funnel() | 퍼널 단계를 분류하는 커스텀 함수. 숫자 접두어(3_, 2_)를 붙여 sort_index() 시 자동 정렬 |
| .apply(함수명) | 각 행에 함수를 적용. for문 없이 열 전체에 한 번에 적용하는 pandas의 핵심 패턴 |
# addtocart는 있지만 transaction이 없는 방문자 필터링
check_list = []
for event_set in visitor_events['event_set']:
if 'addtocart' in event_set and 'transaction' not in event_set:
check_list.append(True)
else:
check_list.append(False)
cart_no_purchase = visitor_events[check_list].copy()
# 이 방문자들이 담은 상품 목록 추출 → 리타겟팅용 CSV 저장
cart_items = events_enriched[
(events_enriched['event'] == 'addtocart') &
(events_enriched['visitorid'].isin(cart_no_purchase['visitorid']))
][['visitorid', 'itemid', 'categoryid', 'timestamp']].copy()
cart_items.to_csv('retargeting_segment_cart_abandon.csv', index=False)실무 활용: 이 사용자 목록을 메타·구글 광고 플랫폼에 업로드하거나, CRM 시스템에 연동하여 "장바구니 리마케팅 이메일"을 발송하는 데 활용합니다. 데이터에서 바로 광고 대상을 추출하는 것이 그로스 마케팅의 핵심 실무입니다!
# 주차별 이벤트 타입 집계 → 전환율 계산
weekly_funnel = (
events_enriched.groupby(['week', 'event'])
.size().unstack(fill_value=0).reset_index()
)
weekly_funnel['view_to_cart_rate'] = (
weekly_funnel['addtocart'] / weekly_funnel['view'] * 100
).round(2)
weekly_funnel['cart_to_transaction_rate'] = (
weekly_funnel['transaction'] / weekly_funnel['addtocart'] * 100
).round(2)
# 시각화
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
axes[0].plot(weekly_funnel['week'], weekly_funnel['view_to_cart_rate'],
marker='o', color='#7B6CF6', linewidth=2)
axes[0].set_title('주차별 view → addtocart 전환율')
axes[1].plot(weekly_funnel['week'], weekly_funnel['cart_to_transaction_rate'],
marker='o', color='#FB923C', linewidth=2)
axes[1].set_title('주차별 addtocart → transaction 전환율')
plt.tight_layout()
plt.show()6전처리 작업 전체 요약
이번 강의의 핵심 개념 3가지
전자상거래 데이터의 특수성
이벤트 데이터는 각각 독립적으로 보지 않고 구매 여정의 흐름으로 봐야 합니다. 어떤 이벤트가 결측치인지 오류인지는 비즈니스 맥락을 이해해야 판단할 수 있습니다.
Long 형식과 Wide 형식
실무 데이터는 장기 보관에 유리한 Long 형식으로 저장됩니다. 분석을 위해 Wide 형식으로 변환하는 것은 필수적인 전처리 과정입니다.
퍼널 분석의 실무 출력물
전환율 수치 자체보다 "어느 구간에서 왜 이탈하는가"에 대한 가설과, 그 가설을 검증하기 위한 세그먼트(예: 장바구니 이탈 사용자)를 추출하는 것이 진짜 업무입니다.
'부트캠프' 카테고리의 다른 글
| 멋쟁이사자처럼 부트캠프 그로스마케팅 4기 32일차_260423 (0) | 2026.04.23 |
|---|---|
| 멋쟁이사자처럼 부트캠프 그로스마케팅 4기 31일차_260422 (1) | 2026.04.22 |
| 멋쟁이사자처럼 부트캠프 그로스마케팅 4기 29일차_260420 (0) | 2026.04.20 |
| 멋쟁이사자처럼 부트캠프 그로스마케팅 4기 28일차_260417 (1) | 2026.04.17 |
| 멋쟁이사자처럼 부트캠프 그로스마케팅 4기 27일차_260416 (0) | 2026.04.16 |