본문 바로가기

데이터 분석/프로젝트

[Python] 사용자 행동 로그 데이터를 활용한 퍼널 분석

반응형

퍼널 분석(Funnel Analysis)은 고객이 유입되고 전환에 이르기까지 주요 단계를 수치로 확인하는 분석 방법입니다. 

전환에 이르는 고객 경험 과정을 단계별로 나누어 언제, 어디서, 어떻게 이탈하는지를 파악해 개선점을 찾아 전환율을 높이기 위한 목적을 가지고 있습니다. 

 

이러한 퍼널 분석은 비즈니스 성과를 향상시키는데 중요한 역할을 하기 때문에 이번 분석에서 사용자 행동 로그 데이터를 통해 분석을 진행해 보겠습니다. 


 

 

 

1. 데이터 소개

한 전자 상거래 웹사이트에서 2020년 1월에 수집된 데이터로, 고객이 상품을 조회하고 구매해난 과정에서 발생한 로그 데이터입니다.

 

 

  • event_time : 이벤트가 발생한 시각
  • event_type: 이벤트의 종류
    • view: 상품을 조회한 경우
    • cart: 상품을 장바구니에 추가한 경우
    • remove_from_cart: 상품을 장바구니에서 제거한 경우
    • purchase: 구매를 한 경우
  • product_id: 상품 번호
  • category_id: 카테고리 번호
  • category_code: 카테고리명
  • brand: 브랜드
  • price: 상품 가격
  • user_id:고객번호
  • user_session: 세션 (유저가 앱을 켜서 끌 때까지 하나의 세션을 유저 세션이라 함)

 


 

 

2. 분석 목적

데이터를 활용하여 확인하고 싶은 내용은 크게 3가지입니다.

 

✔️ DAU(일간 활성 사용자수) 추이는?

      -고객은 어느 요일에 가장 많이 방문하는가?

✔️  사이트 체류시간 평균은?

      - 조회만 한 유저, 장바구니에 담은 유저, 구매까지 한 유저별 체류시간 비교

✔️  퍼널 분석

      - 어느 단계에서 가장 많은 이탈이 발생하는가?

 


 

3. 데이터 전처리

분석을 진행하기 위해 전처리를 진행하겠습니다.

 

data.info()

 

event_time 변수를 보시면 현재 object 타입으로 구성되어 있습니다.

하지만 event_time은 시간을 나타내는 변수이기 때문에 to_datetime 함수를 사용해서 데이터 타입을 변경해 주겠습니다.

 

# 데이터 타입 변경
data['event_time'] = pd.to_datetime(data['event_time'], format= '%Y-%m-%d %H:%M:%S UTC')

 

 

데이터에 결측치가 존재하는지 확인해 보겠습니다.

data.isna().sum()

 

 

 

category_code, brand 컬럼에 상당수의 결측치가 존재하네요. 이번 분석에서는 카테고리나 브랜드에 따른 분석을 진행할 계획이 없음으로 두 컬럼을 제거하겠습니다.

data.drop(['category_code', 'brand'], axis= 1, inplace=True)
data.head(3)

 

 

 

분석의 편의를 위해 event_time변수에서 시간을 제거하고 날짜만 추출해서 date_ymd라는 변수를 생성하겠습니다.

data['date_ymd'] = data['event_time'].dt.date # 시간 제거하고 날짜만
data['date_ymd'] = pd.to_datetime(data['date_ymd'], format= '%Y-%m-%d') #데이터 타입 변경
data.head(3)

 

 

 

이것으로 전처리가 완료되었습니다.


 

3. 데이터 분석

DAU( Daily Active Users ) 추이

첫 번째로 알아볼 내용은 일간 활성 사용자수인 DAU입니다. 

 

이를 구하기 위해선 우선 방문자수 활성 사용자수의 차이에 대해 알아야 합니다.

두 지표는 웹사이트나 애플리케이션의 사용자 활동을 측정하는 지표라는 점에서 유사하지만 디테일한 의미는 다릅니다,

 

방문자 수(Visitors)는 특정 기간 동안 웹사이트나 애플리케이션에 접속한 사용자 수를 의미합니다.

이 지표는 모든 방문자를 포함하며, 방문한 페이지 수나 머문 시간에 관계없이 단순히 웹사이트에 접속한 사람의 수를 나타냅니다.

 

반면, 활성 사용자 수(Active Users)는 특정 기간 동안 웹사이트나 애플리케이션에서 특정 행동(로그인, 상품 조회, 구매, 게시물 작성 등)을 실제로 수행한 사용자 수를 의미합니다. 이를 측정하기 위해 각 디바이스마다 UUID(Unique User ID)를 부여하여 특정 기간마다 단 1회만 측정하여 사용자 수를 계산합니다. 

 

예를 들어 홍길동이라는 사용자가 한 사이트에 3번 접속을 했다면, 방문수 3회이지만 활성 사용자수는 1명이 됩니다. 

(활성 사용자 개념 참고  https://www.openads.co.kr/content/contentDetail?contsId=1296)

 

활성 사용자 수는 DAU, WAU, MAU 가 있는데.

이번 분석에는 일간 활성 사용자 수를 나타내는 DAU의 추이에 대해 알아보겠습니다.

 

 

그러기 위해서는 groupby 함수를 사용하여 date_ymd별 유저아이디의 고유개수를 구해주면 됩니다. 

dau = data.groupby('date_ymd')['user_id'].nunique().reset_index().rename({'user_id': 'dau'}, axis = 1)
dau

 

fig = px.line(data_frame= dau, x = 'date_ymd', y = 'dau', title= 'DAU 추이')
fig.show()

 

시각화 결과, 1월 초부터 중순까지 DAU가 상승하는 추이를 보이고 있고, 그 이후에는 유지되고 있다는 것을 알 수 있습니다.

빨간색 원으로 표시된 날짜는 각각 1월 18일과 25일인데,

이때 동일하게 이용자 수가 감소한 패턴을 보이고 있고, 동일한 요일이기 때문에 요일에 어떠한 영향이 있을 것이라 추측하여 요일별 DAU의 평균을 구해 주기성이 있는지 확인해 보겠습니다.

 

 

요일에 대한 변수를 추가합니다. 

# 요일별로 DAU 추이에 차이가 있는지 
dau['day_of_week'] = dau['date_ymd'].dt.day_name()
dau['day_of_week1'] = dau['date_ymd'].dt.day_of_week # 요일을 숫자로 표현
dau.head()

 

 

 

groupby 함수를 통해 요일별 평균을 계산합니다.

# 요일별 평균
avg_dau_by_dow = dau.groupby(['day_of_week', 'day_of_week1'])['dau'].mean().reset_index()
avg_dau_by_dow.sort_values('day_of_week1', inplace= True)
avg_dau_by_dow

 

 

 

fig = px.bar(data_frame= avg_dau_by_dow, x = 'day_of_week', y = 'dau', title = '요일별 DAU 평균', height= 400, width= 600)
fig.show()

 

시각화 결과. 화요일의 평균이 가장 높고 주말로 갈수록 내려가는 것을 확인할 수 있습니다.

현재 데이터는 20년 1월 한 달 동안의 정보에 한정되어 있어 이 경향을 단정할 수는 없지만, 

더 긴 기간 동안 동일한 패턴이 지속된다면 주말로 갈수록 이용자 수가 줄어드는 경향이 있다고 볼 수 있습니다. 

 


 

사이트 체류 시간 평균

두 번째로 알아볼 내용은 사이트 체류 시간의 평균입니다.

전체 평균과 더불어 각 퍼널 별 체류시간을 비교해 보겠습니다.

(사이트에 들어와 상품을 조회만 한 유저, 카트에 담은 유저, 구매까지 한 유저 별 체류시간)

 

우선 체류 시간은 한 세션의 끝에서 시작 시간을 뺀 값을 말합니다. 

 

 

임의로 유저 세션 하나를 출력하여 체류 시간을 구해보겠습니다. 

data.query('user_session == "546f6af3-a517-4752-a98b-80c4c5860711"').sort_values('event_time')

 

 

이 고객의 2020년 1월 1일 0시 24분에서부터 8시 44분까지 활동했습니다.  

그럼 해당 고객의 체류 시간은 max 값인 8:44에서 min 값 0:24를 뺀 8시간 20분이 되겠네요.

 

 

이제 유저 별 체류 시간의 평균을 구해보겠습니다. 

그러기 위해선 우선 session별 envent_time의 최대, 최소값을 알아야 합니다.

# 유저별 사이트 체류시간 평균
duration = data.groupby('user_session')[['event_time']].agg(['max','min']).reset_index()
duration.head(5)

 

 

max 값에서 min값을 빼면 체류시간을 구할 수 있습니다. 

duration['duration'] = duration['event_time']['max'] - duration['event_time']['min']
duration.head(10)

 

 

보기 좋게 컬럼명을 다음과 같이 수정하겠습니다.

duration.columns = ['user_session', 'max', 'min', 'duration']

 

 

이제 체류 시간 평균을 구하면,

# 체류시간 평균 구하기
duration['duration'].mean()

 

약 한 시간 정도임을 알 수 있습니다.

 

 

 

퍼널 별 체류 시간을 파악하기 위해, pivot table을 활용하여 유저 세션별 퍼널을 구분하겠습니다.

session_pivot = pd.pivot_table(data = data, index= 'user_session', columns= 'event_type', values='event_time', aggfunc= 'count').reset_index().fillna(0)
session_pivot.head(10)

 

저희가 궁금한 건 각 퍼널별 체류시간이기 때문에, 구해진 테이블에서 각 퍼널별 세션을 구분하는 작업이 필요합니다. 

 

event_type = 8인 경우, cart 값이 3입니다. 해당 세션은 상품 조회 후 장바구니에 담는 과정까지 진행한 것으로, 'cart' 퍼널에 해당하는 유저입니다.

 

따라서 cart 또는 purchase 값이 0 이상이면 그 세션에 해당한다고 할 수 있습니다.

 

동일한 원리를 적용하여 장바구 세션과 구매 세션 유저를 구분해 보겠습니다.

 

 

세션의 구분은 query 함수를 사용하면 쉽게 구할 수 있습니다.

# 카트 세션과, 구매 세션 구분
cart_session = list(session_pivot.query('cart > 0')['user_session'])
purchase_session = list(session_pivot.query('purchase > 0')['user_session'])

 

또한, cart_session에 속하지 않으면서 purchase_session에도 속하지 않으면 view에 해당하는 session이 됩니다. 

 

 

 

이제 각 퍼널별 체류 시간의 평균을 구해보겠습니다.

view_sesseion_avg_duration = duration.query('user_session not in @cart_session and user_session not in @purchase_session')['duration'].mean()
cart_sesstion_avg_duration = duration.query('user_session in @cart_session')['duration'].mean()
purchase_sesstion_avg_duration = duration.query('user_session in @purchase_session')['duration'].mean()

print(f'조회만 한 유저의 평균 체류시간, {view_sesseion_avg_duration}')
print(f'카트에 담은 유저의 평균 체류시간, {cart_sesstion_avg_duration}')
print(f'구매까지 한 유저의 평균 체류시간, {purchase_sesstion_avg_duration}')

 

 

다음 퍼널 단계로 넘어가는 유저의 경우, 평균 체류시간이 38분 → 2시간 39분 → 6시간 42분으로 증가한다는 것을 알 수 있습니다.

 


③ 퍼널 분석

마지막으로 퍼널 분석을 진행하여 어느 단계에서 가장 많은 유저들이 이탈하는지 알아보겠습니다.

 

 

위에서 구한 session_pivot 테이블을 이용하겠습니다. 

session_pivot

 

 

각 이벤트 타입별 카운트를 진행한 후, 데이터 프레임 형식으로 저장하겠습니다.

funnel = session_pivot[['view', 'cart', 'remove_from_cart', 'purchase']].sum().to_frame().reset_index()
funnel

 

 

컬럼명을 수정하겠습니다. 

funnel.columns = ['event_type', 'count']
funnel

 

 

원 데이터의 퍼널은 총 4단계이지만 장바구니에서 제거한 경우는 관심의 대상이 아니므로, 해당 퍼널에 해당하는 데이터는 제거했습니다.

funnel = funnel.query('event_type != "remove_from_cart"')
funnel

 

 

 

구해진 퍼널에 대해 시각화를 해 보겠습니다. 

fig = px.funnel(data_frame= funnel, x = 'event_type', y = 'count')
fig.update_traces(texttemplate= '%{value:,.0f}') # 숫자 형식 지정
fig.show()

상품 조회가 100%라고 했을 때, 장바구니 단계는 절반 정도, 장바구니에서 구매로는 훨씬 큰 이탈이 발생한 것을 알 수 있습니다.

 

 

 

단계별 전환율도 구해보겠습니다. 

각 퍼널 단계별 전환율을 계산하는 방법은 4단계의 퍼널 (A → B → C →  D)이 있다고 할 때, 다음과 같습니다. 

 

  • A  →  B 전화율 : B 단계의 사용자 수 / A 단계의 사용자 수
  • B →  C 전환율 : C 단계의 사용자 수 / B 단계의 사용자 수
  • C →  D 전환율 : D 단계의 사용자 수 / C 단계의 사용자 수 

 

전환율을 계산한 후 시각화까지 진행해 보겠습니다. 

#전환율 계산
view_to_cart_rate = list(funnel['count'])[1] / list(funnel['count'])[0]
view_to_purchase_rate = list(funnel['count'])[2] / list(funnel['count'])[1]

funnel['retain_rate'] = [1,view_to_cart_rate ,  view_to_purchase_rate

#시각화
fig = px.funnel(data_frame = funnel, x = 'event_type', y = 'retain_rate')
fig.update_traces(texttemplate = '%{value:,.2%}')
fig.show()

시각화 결과, 조회에서 장바구니로 넘어간 고객의 전환율은 47%, 장바구니에서 구매의 전환율은 19%입니다.

 

전환율과 이탈률은 서로 상호보완적인 관계로, 전환율 = 1 - 이탈률로 표현할 수 있습니다. 

 

  • 조회 → 장바구니 이탈률: 53%
  • 장바구니 → 구매 이탈률: 81%

 

이를 통해 해당 웹사이트에서는 조회에서 장바구니로 넘어가는 과정보다, 장바구니에서 구매로 넘어가는 과정에서 이탈률이 훨씬 높다는 것을 알 수 있습니다.

 

따라서 장바구니에서 구매로 넘어가는 단계의 전환율을 높이기 위한 전략이 필요해 보입니다.

예를 들어 주문서 작성 과정, 주문 시 제공되는 혜택, 결제 과정 등에 문제가 없는지 드릴 다운을 통해 허들을 파악한 후, 이에 맞는 개선 전략을 수립할 수 있습니다.

 

 

 

 

 

 

 

이것으로 사용자 행동 로그데이터 분석이 끝났습니다.

지금까지 읽어주셔서 감사합니다:) 

 

 

 

반응형