> >
Post

[Data Analyst] Funnel Analysis

[Funnel Analysis] Python을 활용한 퍼널 분석 실습 내용을 다룹니다.

[Data Analyst] Funnel Analysis

들어가며


이번 포스팅에선 퍼널 분석(Funnel Analysis)에 대해 다룰 예정이다.
저번 포스팅과 마찬가지로 Python을 사용하여 퍼널 분석 실제로 해보면서, 인사이트를 도출하는 방법을 알아보려한다.



1. 퍼널 분석 (Funnel Analysis)이란 ?


퍼널 분석은 사용자가 특정 목표(예: 구매, 회원가입, 결제 등)에 도달하는 과정을 단계별로 추적하고,
각 단계에서 얼마나 많은 사용자가 이탈하는지를 분석하는 방법론이다.



퍼널의 정의와 유형

  • 고객 여정 퍼널: 사용자가 서비스 내에서 이동하는 과정 (예: 방문 → 장바구니 → 결제)
  • 마케팅 퍼널*: 광고 또는 캠페인에서 고객이 유입되고 전환되는 과정
  • 제품 사용 퍼널: 특정 기능을 사용하기까지의 흐름을 분석

분석의 주요 목적

  • 전환율(Conversion Rate) 최적화
  • 주요 이탈 지점 파악 및 개선
  • 사용자 행동 인사이트 도출
  • 마케팅 및 제품 전략 수립

분석 방법

  1. 퍼널 단계 정의: 사용자의 주요 이동 경로 및 목표 설정
  2. 지표 선정: 각 단계의 전환율 및 이탈률 측정
  3. 데이터 수집: 사용자 행동 데이터 추출 및 정제
  4. 시각화: 퍼널 차트를 활용해 단계별 전환율 시각화
  5. 개선 전략 수집: 이탈 원인 분석 후 최적화 방안 도출



2. 퍼널 분석 시작하기


이제 퍼널 분석의 개념을 이해했으니, 실제로 Python을 사용해 퍼널 분석을 시작해 보겠다.
데이터는 캐글에 E-commerce 행동 데이터를 사용하여 진행했다.
아래는 퍼널 분석을 수행하는 방법을 간단히 단계별로 설명한 내용이다.

Library

1
2
3
4
5
6
7
8
9
import kagglehub
import os
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
import set_matplotlib_hangul
import plotly.express as px
1
2
3
4
5
# Download latest version
path = kagglehub.dataset_download("mkechinov/ecommerce-events-history-in-cosmetics-shop")

print("Path to dataset files:", path)
print(os.listdir(path)) # 다운로드된 파일 목록 확인
Warning: Looks like you're using an outdated `kagglehub` version, please consider updating (latest version: 0.3.10)
Path to dataset files: /Users/mungughyeon/.cache/kagglehub/datasets/mkechinov/ecommerce-events-history-in-cosmetics-shop/versions/6
['2020-Jan.csv', '2019-Dec.csv', '2020-Feb.csv', '2019-Nov.csv', '2019-Oct.csv']
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
files = ["2020-Jan.csv", "2019-Dec.csv", "2020-Feb.csv", "2019-Nov.csv", "2019-Oct.csv"]

dfs = {}
min_rows = float('inf')  # 최소 row 수를 저장할 변수
min_df = None  # 가장 작은 데이터프레임을 저장할 변수

for file in files:
    file_path = os.path.join(path, file)  # 파일 경로 생성
    df = pd.read_csv(file_path)  # CSV 파일 로드
    dfs[file] = df  # 딕셔너리에 저장
    
    print(f"{file}: {df.shape}")  
    
    # row 수가 가장 적은 데이터프레임 찾기
    if df.shape[0] < min_rows:
        min_rows = df.shape[0]
        min_df = df

# 가장 row 수가 적은 데이터프레임을 df에 저장
df = min_df
df
2020-Jan.csv: (4264752, 9)
2019-Dec.csv: (3533286, 9)
2020-Feb.csv: (4156682, 9)
2019-Nov.csv: (4635837, 9)
2019-Oct.csv: (4102283, 9)
event_timeevent_typeproduct_idcategory_idcategory_codebrandpriceuser_iduser_session
02019-12-01 00:00:00 UTCremove_from_cart57127901487580005268456287NaNf.o.x6.2757680293251d85cb0-897f-48d2-918b-ad63965c12dc
12019-12-01 00:00:00 UTCview57646551487580005411062629NaNcnd29.054121200928adff31e-2051-4894-9758-224bfa8aec18
22019-12-01 00:00:02 UTCcart49581487580009471148064NaNrunail1.19494077766c99a50e8-2fac-4c4d-89ec-41c05f114554
32019-12-01 00:00:05 UTCview58484131487580007675986893NaNfreedecor0.79348405118722ffea5-73c0-4924-8e8f-371ff8031af4
42019-12-01 00:00:07 UTCview58241481487580005511725929NaNNaN5.5657600568328172809-7e4a-45ce-bab0-5efa90117cd5
..............................
35332812019-12-31 23:59:39 UTCview56833501487580005671109489NaNmasura2.84536812729e4a2d47c-a956-4c46-8176-745f52ea664b
35332822019-12-31 23:59:46 UTCview58880971487580013388628160NaNshik179.055036581542dde9867-9e71-4a64-880d-aa68b66aae6d
35332832019-12-31 23:59:51 UTCview599751487580012096782476NaNNaN7.145954145414c6d80bb-5dd3-4fbb-b592-187b51db2753
35332842019-12-31 23:59:52 UTCview57759821783999063314661546NaNNaN11.903977808787e8a2b85-153a-44eb-a71f-b748fde14fcc
35332852019-12-31 23:59:57 UTCview56350901487580005754995573NaNNaN4.44420652863546f6af3-a517-4752-a98b-80c4c5860711

3533286 rows × 9 columns

1
2
3
4
5
warnings.filterwarnings("ignore", category=FutureWarning)

# df.to_csv("Funnel.csv") # 저장
df = pd.read_csv("../data/Funnel.csv", index_col=0) # Load Data
df
event_timeevent_typeproduct_idcategory_idcategory_codebrandpriceuser_iduser_session
02019-12-01 00:00:00 UTCremove_from_cart57127901487580005268456287NaNf.o.x6.2757680293251d85cb0-897f-48d2-918b-ad63965c12dc
12019-12-01 00:00:00 UTCview57646551487580005411062629NaNcnd29.054121200928adff31e-2051-4894-9758-224bfa8aec18
22019-12-01 00:00:02 UTCcart49581487580009471148064NaNrunail1.19494077766c99a50e8-2fac-4c4d-89ec-41c05f114554
32019-12-01 00:00:05 UTCview58484131487580007675986893NaNfreedecor0.79348405118722ffea5-73c0-4924-8e8f-371ff8031af4
42019-12-01 00:00:07 UTCview58241481487580005511725929NaNNaN5.5657600568328172809-7e4a-45ce-bab0-5efa90117cd5
..............................
35332812019-12-31 23:59:39 UTCview56833501487580005671109489NaNmasura2.84536812729e4a2d47c-a956-4c46-8176-745f52ea664b
35332822019-12-31 23:59:46 UTCview58880971487580013388628160NaNshik179.055036581542dde9867-9e71-4a64-880d-aa68b66aae6d
35332832019-12-31 23:59:51 UTCview599751487580012096782476NaNNaN7.145954145414c6d80bb-5dd3-4fbb-b592-187b51db2753
35332842019-12-31 23:59:52 UTCview57759821783999063314661546NaNNaN11.903977808787e8a2b85-153a-44eb-a71f-b748fde14fcc
35332852019-12-31 23:59:57 UTCview56350901487580005754995573NaNNaN4.44420652863546f6af3-a517-4752-a98b-80c4c5860711

3533286 rows × 9 columns

1
2
# 데이터 타입 확인
df.info()
<class 'pandas.core.frame.DataFrame'>
Index: 3533286 entries, 0 to 3533285
Data columns (total 9 columns):
 #   Column         Dtype  
---  ------         -----  
 0   event_time     object 
 1   event_type     object 
 2   product_id     int64  
 3   category_id    int64  
 4   category_code  object 
 5   brand          object 
 6   price          float64
 7   user_id        int64  
 8   user_session   object 
dtypes: float64(1), int64(3), object(5)
memory usage: 269.6+ MB


Step 1: 데이터 전처리

본격적인 퍼널 분석을 수행하기 전에 적합한 형태로 변환하는 과정이다.
현재 event_time 변수를 보면 object 타입으로 구성돼 있지만
시간을 나타내는 변수이기 때문에 to_datetime 함수를 사용해서 데이터 타입을 변경해 주겠다.

1
2
# 결측치 확인
df.isna().sum()
event_time             0
event_type             0
product_id             0
category_id            0
category_code    3474821
brand            1510289
price                  0
user_id                0
user_session         779
dtype: int64
1
2
3
# 결측치 컬럼 제거
df.drop(columns={'category_code', 'brand', 'user_session'}, inplace=True)
df.head()
event_timeevent_typeproduct_idcategory_idpriceuser_id
02019-12-01 00:00:00 UTCremove_from_cart571279014875800052684562876.27576802932
12019-12-01 00:00:00 UTCview5764655148758000541106262929.05412120092
22019-12-01 00:00:02 UTCcart495814875800094711480641.19494077766
32019-12-01 00:00:05 UTCview584841314875800076759868930.79348405118
42019-12-01 00:00:07 UTCview582414814875800055117259295.56576005683
1
2
3
4
df['event_time'] = pd.to_datetime(df['event_time']) # 시게열 타입으로 변환
df['Date'] = df['event_time'].dt.date # 일자별 컬러 생성
df['Date'] = pd.to_datetime(df['Date'])
df
event_timeevent_typeproduct_idcategory_idpriceuser_idDate
02019-12-01 00:00:00+00:00remove_from_cart571279014875800052684562876.275768029322019-12-01
12019-12-01 00:00:00+00:00view5764655148758000541106262929.054121200922019-12-01
22019-12-01 00:00:02+00:00cart495814875800094711480641.194940777662019-12-01
32019-12-01 00:00:05+00:00view584841314875800076759868930.793484051182019-12-01
42019-12-01 00:00:07+00:00view582414814875800055117259295.565760056832019-12-01
........................
35332812019-12-31 23:59:39+00:00view568335014875800056711094892.845368127292019-12-31
35332822019-12-31 23:59:46+00:00view58880971487580013388628160179.055036581542019-12-31
35332832019-12-31 23:59:51+00:00view5997514875800120967824767.145954145412019-12-31
35332842019-12-31 23:59:52+00:00view5775982178399906331466154611.903977808782019-12-31
35332852019-12-31 23:59:57+00:00view563509014875800057549955734.444206528632019-12-31

3533286 rows × 7 columns


Step 2: 사용자 이벤트 분석

전처리를 완료했으니, 이제 사용자들이 어떤 이벤트를 얼마나 수행했는지 분석해보자.
퍼널 분석에서는 각 단계별로 몇 명의 사용자가 남아 있는지, 그리고 어디에서 이탈하는지를 파악하는 것이 핵심이다.
각 퍼널 단계별 DAU( Daily Active Users) 추출

DAU란?

DAU 일간 활성 사용자 수 는 하루 동안 최소 1회 이상 특정 이벤트를 수행한 고유 사용자 수를 의미한다.
이는 사용자의 서비스 참여도를 측정하는 핵심 지표이며, 일별 사용자 활동량과 서비스 성장성을 평가하는 데 활용된다.


  • 여러 분석을 하기전에 시각화 함수부터 만들어 주겠다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 시각화 함수
class Funnel_Analysis:

    # bar_plot 함수
    @staticmethod    
    def bar_plot(df, x_col, y_col, figsize=(10, 6), palette="Blues_r", rotation=None, title=None, xlabel=None, ylabel=None):
        plt.figure(figsize=figsize)
        sns.barplot(x=df[x_col], y=df[y_col], data=df, palette=palette)
        plt.xticks(rotation=rotation)
        plt.title(title)
        plt.xlabel(xlabel)
        plt.ylabel(ylabel)
        plt.show()

    # box_plot 함수
    @staticmethod
    def box_plot(df, x_col, y_col, figsize=(8, 6), palette=None, title=None, xlabel=None, ylabel=None):
        plt.figure(figsize=figsize)
        sns.boxplot(data=df, x=x_col, y=y_col, palette=palette)
        plt.xticks(rotation=90)
        plt.title(title, fontsize=14)
        plt.xlabel(xlabel, fontsize=12)
        plt.ylabel(ylabel, fontsize=12)
        plt.show()

    # line_plot 함수
    @staticmethod
    def plot_lineplot(df, x_col, y_col, hue=None, figsize=(10, 6), palette=None, marker='o', linewidth=2, rotation=None, title=None, xlabel=None, ylabel=None):
        plt.figure(figsize=figsize)
        
        # hue가 None이면 일반 lineplot, hue가 있으면 그룹별 lineplot
        sns.lineplot(data=df, x=x_col, y=y_col, hue=hue, palette=palette, marker=marker, linewidth=linewidth)
        plt.xticks(rotation=rotation)  
        plt.title(title if title else f'{y_col} by {x_col} (Line Plot)')  
        plt.xlabel(xlabel)
        plt.ylabel(ylabel)
        plt.grid(True)
        if hue: # hue가 있을 경우 범례 추가
            plt.legend(title=hue)
        plt.show()


  • 이벤트 타입별 사용자 수
1
2
3
4
5
6
7
# 각 사용자별 'event_type' 일수 계산
funnel_df = df.groupby(['user_id', 'event_type'])['Date'].nunique().unstack().fillna(0)
event_counts = funnel_df.sum().sort_values(ascending=False).reset_index() # 이벤트 별 사용자 수
event_counts.columns = ['event_type', 'unique_users']

# 시각화 함수
Funnel_Analysis.bar_plot(df=event_counts, x_col='event_type', y_col='unique_users', xlabel='이벤트 타입', ylabel='사용자 수', title='이벤트 타입별 고유 사용자 수')


  • 전체 DAU 분석
1
2
dau = df.groupby('Date')['user_id'].nunique().reset_index().rename({'user_id': 'DAU'}, axis = 1)
Funnel_Analysis.plot_lineplot(df=dau, x_col='Date', y_col='DAU', title='DAU 변화', xlabel='날짜', ylabel='DAU', rotation=45)


  • 요일별 DAU 변화 분석
1
2
3
4
5
6
7
8
# 요일 컬럼 추가 (숫자+이름)
dau['day_of_week'] = dau['Date'].dt.day_of_week  # 0=Monday, 6=Sunday
dau['Week'] = dau['Date'].dt.day_name()
week_dau = dau.groupby(['day_of_week', 'Week'])['DAU'].sum().reset_index()
week_dau = week_dau.sort_values('day_of_week')

# 시각화
Funnel_Analysis.bar_plot(df=week_dau, x_col='Week', y_col='DAU', title='요일별 DAU 분석', xlabel='요일', ylabel='DAU')


  • 이벤트별 DAU 변화 분석
1
2
3
4
5
6
7
# 이벤트별 DAU 계산 (일별 이벤트 수행 사용자 수)
event_dau_df = df.groupby(['Date', 'event_type'])['user_id'].nunique().unstack().fillna(0)
event_dau_melted = event_dau_df.reset_index().melt(id_vars=['Date'], var_name='event_type', value_name='DAU')
event_dau_melted = event_dau_melted[event_dau_melted['event_type'] != 'remove_from_cart'] # '장바구니 삭제'는 퍼널에서 제외

# 이벤트별 DAU 변화 시각화
Funnel_Analysis.plot_lineplot(df=event_dau_melted, x_col='Date', y_col='DAU', hue='event_type', title='이벤트별 DAU 변화', xlabel='날짜', ylabel='DAU', rotation=45)


  • 이번엔 요일별 DAU 분석
1
2
3
4
5
6
7
8
event_dau_melted['day_of_week'] = event_dau_melted['Date'].dt.day_of_week # 0=Monday, 6=Sunday
event_dau_melted['Week'] = event_dau_melted['Date'].dt.day_name()
event_week_dau = event_dau_melted.groupby(['day_of_week', 'Week', 'event_type'])['DAU'].sum().reset_index()
event_week_dau = event_week_dau[event_week_dau['event_type'] != 'remove_from_cart']
event_week_dau = event_week_dau.sort_values('day_of_week')

# 시각화
Funnel_Analysis.plot_lineplot(df=event_week_dau, x_col='Week', y_col='DAU', hue='event_type', title='이벤트 요일별 DAU 분석', xlabel='요일', ylabel='DAU')


Step 3: 체류 시간 분석

이번엔 사이트 체류 시간에 대해 알아보려 한다.
사용자의 사이트 체류 시간은 서비스의 사용자 경험(UX) 및 이탈률을 평가하는 중요한 지표이다.
체류 시간이 길수록 관심이 높고, 짧을수록 개선이 필요할 가능성이 있다.
또한, 퍼널 분석의 주목적은 구매까지의 전환율을 분석하는 것이기에, remove_from_cart(장바구니 삭제)는 제외

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
df = df.sort_values(by=['user_id', 'event_time'])
df['prev_time'] = df.groupby('user_id')['event_time'].shift(1) # # 이전 이벤트 시간 계산
df['time_diff'] = (df['event_time'] - df['prev_time']).dt.total_seconds() # 초 단위 변환
df['new_session'] = df['time_diff'].apply(lambda x: 1 if x > 1800 else 0) # 30분(1800초) 이상이면 새로운 세션 시작
df['session_id'] = df.groupby('user_id')['new_session'].cumsum() # 세션 번호 증가

# 유저별 이벤트 세션 체류 시간 계산
user_event_session_time = df.groupby(['user_id', 'event_type', 'session_id'])['time_diff'].sum().reset_index()
user_avg_event_time = user_event_session_time.groupby(['user_id', 'event_type'])['time_diff'].mean().reset_index() # 유저별 평균 체류 시간
event_avg_session_time = user_avg_event_time.groupby('event_type')['time_diff'].mean().reset_index() # 이벤트별 평균 체류 시간 계산
event_avg_session_time['hours'] = (event_avg_session_time['time_diff'] // 3600).astype(int)  # 정수 시간
event_avg_session_time['minutes'] = ((event_avg_session_time['time_diff'] % 3600) / 60).round(0).astype(int)  # 남은 초를 분으로 변환

event_order = ['view', 'cart', 'purchase'] # 정렬
event_avg_session_time = event_avg_session_time.set_index('event_type').loc[event_order].reset_index()
event_avg_session_time
event_typetime_diffhoursminutes
0view40962.3612611123
1cart34120.266202929
2purchase5526.142084132
1
2
3
4
5
# 추가 전처리
event_avg_session_time['formatted_time'] = event_avg_session_time.apply(
    lambda x: f"{x['hours']}시간 {x['minutes']}", axis=1)

event_avg_session_time
event_typetime_diffhoursminutesformatted_time
0view40962.361261112311시간 23분
1cart34120.2662029299시간 29분
2purchase5526.1420841321시간 32분
1
2
3
Funnel_Analysis.bar_plot(
    df=event_avg_session_time, x_col='event_type', y_col='hours', 
    title='이벤트별 체류 시간', xlabel='이벤트', ylabel='체류 시간', palette='Greens_r')


Step 4: 단계별 전환율 분석

이번엔 사용자가 구매 퍼널에서 어느 단계에서 가장 많이 이탈하는지 확인하기 위해 전환율과 이탈율을 분석 해보려한다.
전환율: 이전 단계 대비 다음 단계로 넘어간 사용자 비율
이탈율: 해당 단계에서 빠져나간 사용자 비율

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# event_order = ['view', 'cart', 'purchase'] 

# 퍼널 단계별 사용자 수 계산
funnel_counts = {event: df[df['event_type'] == event]['user_id'].nunique() for event in event_order}

# 전환율 및 이탈율 계산
conversion_rates = {'view': 100}  # View 단계는 전체 사용자의 기준이므로 100% 설정
drop_off_rates = {}

for i in range(len(event_order) - 1):
    current_step = event_order[i]
    next_step = event_order[i + 1]
    
    if funnel_counts[current_step] > 0:
        conversion_rates[next_step] = (funnel_counts[next_step] / funnel_counts[current_step]) * 100  
        drop_off_rates[current_step] = 100 - conversion_rates[next_step]  
    else:
        conversion_rates[next_step] = 0
        drop_off_rates[current_step] = 0

# 마지막 단계(Purchase)의 이탈율은 항상 0%
drop_off_rates[event_order[-1]] = 0  

# 데이터프레임 생성
df_funnel = pd.DataFrame({
    '단계': list(conversion_rates.keys()),
    '전환율 (%)': list(conversion_rates.values()),
    '이탈율 (%)': list(drop_off_rates.values())})

df_funnel
단계전환율 (%)이탈율 (%)
0view100.00000076.701506
1cart23.29849469.310312
2purchase30.6896880.000000
  • 단계별 전환율 분석
1
2
3
4
fig = px.funnel(data_frame=df_funnel, x='단계', y='전환율 (%)', title="단계별 전환율 분석")
fig.update_layout(width=800, height=500)
fig.update_traces(texttemplate='%{value:.2f}%') # 소수점 두 자리
fig.show()

  • 단계별 이탈율 분석
1
2
3
4
5
fig = px.funnel(data_frame=df_funnel, x='단계', y='이탈율 (%)', title="단계별 이탈율 분석")
fig.update_traces(marker=dict(color='red'))  
fig.update_layout(width=800, height=500)
fig.update_traces(texttemplate='%{value:.2f}%') # 소수점 두 자리
fig.show()



마무리


이번 포스팅에서는 퍼널 분석(Funnel Analysis)을 활용하여 사용자의 행동 패턴과 단계별 전환율을 분석해 봤다.
Python을 이용해 DAU 분석, 요일별 트렌드, 체류 시간, 단계별 전환율 및 이탈율을 살펴보며,
사용자가 어디에서 이탈하는지를 파악하는 방법을 다뤘다.
다음 포스팅에서는 A/B 테스트를 통해 퍼널 분석 결과를 기반으로 최적화 전략을 검증하는 방법을 살펴볼 예정이다.
(많관부 🔥🙏)



다음 포스팅


시리즈

This post is licensed under CC BY 4.0 by the author.