프로젝트/GA4 분석

[GA4 퍼널 분석] Day6-2 심층 분석 검증 (User Type·Unknown 구조 재정의)

조성호 2026. 5. 12. 21:32

추가 분석 이유

본편에서는 결제 단계(Begin Checkout → Purchase) 이탈 원인을 Device, User Type, Source, Checkout Value 관점에서 검토한 결과, 핵심 변수는 Device·Source보다 User Type(신규 유저 신뢰 장벽)일 가능성이 높다는 결론에 도달했다. 그러나 “신규 유저가 문제다”라는 결론만으로는 실제 액션 포인트가 부족하다.

 

따라서 심층 분석에서는 “신규 유저 문제는 특정 디바이스 문제인가?”, “특정 유입 채널 문제인가?”, “특정 상품군 구조 문제인가?”를 추가 검증해, User Type 핵심 가설의 실제 구조를 더 세밀하게 분해하고자 했다.

심층 가설 왜 봤는가 검증 목적
User Type × Device 신규 유저가 모바일에서 특히 약한가? UX vs 신뢰 구분
User Type × Source 신규 유저가 특정 채널에서만 약한가? 채널 vs 신뢰 구분
Category Checkout Day5 핵심(Bags)이 결제 단계까지 이어지는가? 퍼널 단계별 병목 위치 확인

왜 심층 분석이 필요했는가?

예를 들어 User Type이 중요하더라도, “신규 유저 전체 문제”인지 “신규 + 모바일 문제”인지 “신규 + Google 유입 문제”인지에 따라 실제 개선 전략은 완전히 달라진다.

핵심 변수의 ‘존재’보다 핵심 변수의 ‘작동 구조’를 확인하는 단계가 필요했다.

사용 방법

Two-Proportion Z-Test

두 그룹 직접 비교

  • New Mobile vs New Desktop
  • Returning Mobile vs Returning Desktop
  • New Google vs New Direct
  • Returning Google vs Returning Direct
  • Google 내 New vs Returning
  • Direct 내 New vs Returning
  • Unknown vs Bags
  • Bags vs Apparel

User Type × Device 심층 분석 – 신규 유저 문제는 모바일 UX 문제인가?

가설

신규 유저가 특히 모바일 환경에서 더 많이 이탈한다면, 신뢰보다 UX 문제가 핵심일 수 있다.

 

전처리

User Type + Device 조합별 checkout_sessions / purchase_sessions를 구성해 New / Returning 내부에서 Mobile vs Desktop 비교 구조를 만들었다.

 

New Mobile vs Desktop (Z-Test)

new_device_df = ud_df[
    ud_df["user_type"] == "New"
].copy()

new_mobile = new_device_df[
    new_device_df["device_category"] == "mobile"
].iloc[0]

new_desktop = new_device_df[
    new_device_df["device_category"] == "desktop"
].iloc[0]

count = [
    new_mobile["purchase_sessions"],
    new_desktop["purchase_sessions"]
]

nobs = [
    new_mobile["checkout_sessions"],
    new_desktop["checkout_sessions"]
]

stat, pval = proportions_ztest(count, nobs)

print("=== New User Mobile vs Desktop ===")
print("New Mobile Rate:", round(new_mobile["checkout_to_purchase_rate_calc"], 2))
print("New Desktop Rate:", round(new_desktop["checkout_to_purchase_rate_calc"], 2))
print("Z-stat:", stat)
print("p-value:", pval)

Returning Mobile vs Desktop (Z-Test)

ret_device_df = ud_df[
    ud_df["user_type"] == "Returning"
].copy()

ret_mobile = ret_device_df[
    ret_device_df["device_category"] == "mobile"
].iloc[0]

ret_desktop = ret_device_df[
    ret_device_df["device_category"] == "desktop"
].iloc[0]

count_ret = [
    ret_mobile["purchase_sessions"],
    ret_desktop["purchase_sessions"]
]

nobs_ret = [
    ret_mobile["checkout_sessions"],
    ret_desktop["checkout_sessions"]
]

stat_ret, pval_ret = proportions_ztest(
    count_ret,
    nobs_ret
)

print("=== Returning Mobile vs Desktop ===")
print("Returning Mobile Rate:", round(ret_mobile["checkout_to_purchase_rate_calc"], 2))
print("Returning Desktop Rate:", round(ret_desktop["checkout_to_purchase_rate_calc"], 2))
print("Z-stat:", stat_ret)
print("p-value:", pval_ret)

해석

New Mobile vs Desktop
p-value = 0.303
유의하지 않음
+4.31% Lift

Returning Mobile vs Desktop
p-value = 0.187
유의하지 않음
+3.16% Lift

 

핵심 해석

신규 / 재방문 모두 Mobile vs Desktop 차이가 유의하지 않았다.

즉, 신규 유저 문제는 특정 디바이스 UX 문제가 아니라 디바이스와 무관한 전반적 신뢰 장벽 가능성이 높았다.

 

실무적 생각

모바일 UX 개선보다 첫 구매 신뢰 / 결제 장벽 완화 우선순위가 더 높다.


User Type × Source 심층 분석 – 신규 유저 문제는 특정 채널 문제인가?

가설

신규 유저가 특정 유입 채널(Google 등)에서만 낮다면, 신뢰보다 채널 품질 문제가 핵심일 수 있다.

 

전처리

User Type + Source 조합별 checkout_sessions / purchase_sessions를 구성했다.

 

New Google vs Direct

# New Google vs Direct
new_source_df = stable_us_df[
    stable_us_df["user_type"] == "New"
].copy()

new_google = new_source_df[
    new_source_df["source"] == "google"
].iloc[0]

new_direct = new_source_df[
    new_source_df["source"] == "(direct)"
].iloc[0]

count = [
    new_google["purchase_sessions"],
    new_direct["purchase_sessions"]
]

nobs = [
    new_google["checkout_sessions"],
    new_direct["checkout_sessions"]
]

stat, pval = proportions_ztest(count, nobs)

print("=== New Google vs Direct ===")
print("New Google Rate:", round(new_google["checkout_to_purchase_rate_calc"], 2))
print("New Direct Rate:", round(new_direct["checkout_to_purchase_rate_calc"], 2))
print("Z-stat:", stat)
print("p-value:", pval)

new_google_rate = new_google["checkout_to_purchase_rate_calc"]
new_direct_rate = new_direct["checkout_to_purchase_rate_calc"]

lift = (
    (new_direct_rate - new_google_rate)
    / new_google_rate
) * 100

print("New Direct vs Google Lift:", round(lift, 2), "%")

Returning Google vs Direct

# Returning Google vs Direct
returning_source_df = stable_us_df[
    stable_us_df["user_type"] == "Returning"
].copy()

returning_google = returning_source_df[
    returning_source_df["source"] == "google"
].iloc[0]

returning_direct = returning_source_df[
    returning_source_df["source"] == "(direct)"
].iloc[0]

count = [
    returning_google["purchase_sessions"],
    returning_direct["purchase_sessions"]
]

nobs = [
    returning_google["checkout_sessions"],
    returning_direct["checkout_sessions"]
]

stat, pval = proportions_ztest(count, nobs)

print("=== Returning Google vs Direct ===")
print("Returning Google Rate:", round(returning_google["checkout_to_purchase_rate_calc"], 2))
print("Returning Direct Rate:", round(returning_direct["checkout_to_purchase_rate_calc"], 2))
print("Z-stat:", stat)
print("p-value:", pval)


returning_google_rate = returning_google["checkout_to_purchase_rate_calc"]
returning_direct_rate = returning_direct["checkout_to_purchase_rate_calc"]

lift = (
    (returning_direct_rate - returning_google_rate)
    / returning_google_rate
) * 100

print("Returning Direct vs Google Lift:", round(lift, 2), "%")

Google 내 New vs Returning

# Google 내 New vs Returning
google_df = stable_us_df[
    stable_us_df["source"] == "google"
].copy()

google_new = google_df[
    google_df["user_type"] == "New"
].iloc[0]

google_returning = google_df[
    google_df["user_type"] == "Returning"
].iloc[0]

count = [
    google_new["purchase_sessions"],
    google_returning["purchase_sessions"]
]

nobs = [
    google_new["checkout_sessions"],
    google_returning["checkout_sessions"]
]

stat, pval = proportions_ztest(count, nobs)

print("=== Google New vs Returning ===")
print("Google New Rate:", round(google_new["checkout_to_purchase_rate_calc"], 2))
print("Google Returning Rate:", round(google_returning["checkout_to_purchase_rate_calc"], 2))
print("Z-stat:", stat)
print("p-value:", pval)


google_new_rate = google_new["checkout_to_purchase_rate_calc"]
google_returning_rate = google_returning["checkout_to_purchase_rate_calc"]

lift = (
    (google_returning_rate - google_new_rate)
    / google_new_rate
) * 100

print("Google Returning vs New Lift:", round(lift, 2), "%")

Direct 내 New vs Returning

# Direct 내 New vs Returning
direct_df = stable_us_df[
    stable_us_df["source"] == "(direct)"
].copy()

direct_new = direct_df[
    direct_df["user_type"] == "New"
].iloc[0]

direct_returning = direct_df[
    direct_df["user_type"] == "Returning"
].iloc[0]

count = [
    direct_new["purchase_sessions"],
    direct_returning["purchase_sessions"]
]

nobs = [
    direct_new["checkout_sessions"],
    direct_returning["checkout_sessions"]
]

stat, pval = proportions_ztest(count, nobs)

print("=== Direct New vs Returning ===")
print("Direct New Rate:", round(direct_new["checkout_to_purchase_rate_calc"], 2))
print("Direct Returning Rate:", round(direct_returning["checkout_to_purchase_rate_calc"], 2))
print("Z-stat:", stat)
print("p-value:", pval)


direct_new_rate = direct_new["checkout_to_purchase_rate_calc"]
direct_returning_rate = direct_returning["checkout_to_purchase_rate_calc"]

lift = (
    (direct_returning_rate - direct_new_rate)
    / direct_new_rate
) * 100

print("Direct Returning vs New Lift:", round(lift, 2), "%")

해석

New Google vs Direct
p-value = 0.318
유의하지 않음

Returning Google vs Direct
p-value = 0.840
유의하지 않음

Google 내 New vs Returning
p-value < 0.001
+99.86% Lift

Direct 내 New vs Returning
p-value < 0.001
+88.58% Lift

 

핵심 해석

Source 내부에서는 New vs Returning 차이가 매우 컸지만, New 내부 / Returning 내부에서는 Source 차이가 거의 없었다.

즉, “어디서 왔는가?”보다 “신규 유저인가?”가 훨씬 중요했다.

 

실무적 생각

채널 최적화보다 신규 유저 신뢰 구조 개선(첫 구매 혜택 / 신뢰 배지 / 게스트 결제)우선순위가 더 높다.


Category Checkout 심층 분석 – Day5 Bags 문제는 결제 단계까지 이어지는가?

가설

Day5에서 Bags가 장바구니 단계 핵심 병목이었다면, 결제 단계에서도 동일하게 약할 가능성이 있다.

 

전처리

Checkout 단계 Category별 purchase_sessions 비교 구조를 만들고, 공백 / (not set) / Uncategorized 계열은 Unknown으로 통합했다.

 

Unknown vs Bags

# Unknown vs Bags
unknown = stable_cat_df[
    stable_cat_df["item_category"] == "Unknown"
].iloc[0]

bags = stable_cat_df[
    stable_cat_df["item_category"] == "Bags"
].iloc[0]

count = [
    unknown["purchase_sessions"],
    bags["purchase_sessions"]
]

nobs = [
    unknown["checkout_sessions"],
    bags["checkout_sessions"]
]

stat, pval = proportions_ztest(count, nobs)

print("=== Unknown vs Bags ===")
print("Unknown Rate:", round(unknown["checkout_to_purchase_rate_calc"], 2))
print("Bags Rate:", round(bags["checkout_to_purchase_rate_calc"], 2))
print("Z-stat:", stat)
print("p-value:", pval)

lift = (
    (bags["checkout_to_purchase_rate_calc"] - unknown["checkout_to_purchase_rate_calc"])
    / unknown["checkout_to_purchase_rate_calc"]
) * 100

print("Bags vs Unknown Lift:", round(lift, 2), "%")

Bags vs Apparel

apparel = stable_cat_df[
    stable_cat_df["item_category"] == "Apparel"
].iloc[0]

count = [
    bags["purchase_sessions"],
    apparel["purchase_sessions"]
]

nobs = [
    bags["checkout_sessions"],
    apparel["checkout_sessions"]
]

stat, pval = proportions_ztest(count, nobs)

print("=== Bags vs Apparel ===")
print("Bags Rate:", round(bags["checkout_to_purchase_rate_calc"], 2))
print("Apparel Rate:", round(apparel["checkout_to_purchase_rate_calc"], 2))
print("Z-stat:", stat)
print("p-value:", pval)

lift = (
    (apparel["checkout_to_purchase_rate_calc"] - bags["checkout_to_purchase_rate_calc"])
    / bags["checkout_to_purchase_rate_calc"]
) * 100

print("Apparel vs Bags Lift:", round(lift, 2), "%")

해석

Unknown vs Bags
p-value < 0.001
+34.53% Lift

Bags vs Apparel
p-value = 0.439
유의하지 않음

 

핵심 해석

Bags는 결제 단계에서 구조적 최악이 아니었다.
오히려 Unknown(카테고리 미분류 / 정보 불완전군)이 실제 핵심 병목이었다.

 

구조적 의미

이전: Bags = 장바구니 진입 구조 문제

현재: Unknown = 결제 단계 구조 문제

 

실무적 생각

Bags

  • 초기 상품 매력도
  • 상세페이지
  • 리뷰
  • 프로모션

Unknown

  • 카테고리 정합성
  • 상품 정보 구조
  • 데이터 품질
  • 결제 신뢰

 

심층 분석 전체 결론

User Type × Device: 디바이스 문제 아님

User Type × Source: 채널 문제 아님

Category Checkout: Bags보다 Unknown 구조가 더 위험

 

최종 구조 재정리

이전

무엇을 담게 할 것인가?

Price + Bags

 

현재

담은 상품을 왜 끝내 사지 않는가?

User Type + Unknown

 

최종 우선순위

Tier 1

User Type (신규 유저 신뢰 장벽)

 

Tier 1.5

Unknown Category (상품 정보/구조 문제)

 

Tier 2

Checkout Value (고가 세그먼트 추가 검토)

 

Tier 3

Device / Source

 

가장 중요한 인사이트

신규 유저 결제 이탈은 “어떤 기기/채널인가”보다 “신뢰가 형성되지 않은 첫 구매 상태인가”가 핵심이었다.

 

그래서 무엇을 해야 하는가?

신규 유저

  • 게스트 결제
  • 첫 구매 혜택
  • 무료배송
  • 신뢰 배지
  • 결제 간소화

 

Unknown 구조

  • 카테고리 정비
  • 상품 정보 보강
  • 상세페이지 품질 개선
  • 데이터 추적 구조 개선

 

최종 결론

“Day5가 상품 경쟁력(Price + Bags)의 문제였다면, Day6 심층 분석 결과 실제 구매 완료를 막는 핵심은 신규 유저 신뢰 장벽(User Type)과 상품 정보 구조 문제(Unknown Category)였다.”

 

복기 – Day5 Category 해석의 한계와 Day6 Unknown 발견

Day6 Category Checkout 심층 분석 과정에서

Unknown(공백 / 미분류 / 정보 불완전군)

카테고리가 결제 단계의 핵심 병목 후보로 새롭게 확인되었다.

이는 Day5 Category 분석 당시 핵심으로 보였던 Bags 중심 해석에 대해,

“초기 전처리 방식이 일부 구조적 문제를 충분히 반영하지 못했을 가능성”

을 다시 점검하게 만들었다.

 

무엇이 문제였는가?

전처리 과정

Category 분석 과정에서 공백값 / 미분류값이 제거되거나 약화되면서, Unknown 계열이 주요 분석 대상에서 충분히 드러나지 않았을 가능성 있으며 실제론 구조적 병목 후보였을 수 있는 Unknown 그룹이 공백 처리 또는 정제 과정에서 해석 우선순위에서 밀렸을 가능성 있다.

 

결과

이전 해석: Bags = 핵심 Category 문제

 

심층 분석: Bags = 초기 퍼널(View → Cart) 문제 / Unknown = 결제 퍼널(Checkout → Purchase) 핵심 문제

 

중요한 복기 포인트

초기 분석에서 “보이는 주요 카테고리” 중심으로 해석하는 것은 중요하지만, 결측 / 미분류 / Unknown 데이터 자체가 구조적 문제일 가능성도 반드시 검토해야 한다.

 

교훈

단순 공백 제거: 분석 정확도 향상 가능

하지만 “왜 공백이 발생했는가?”, “공백 자체가 특정 구조적 문제를 의미하는가?”까지 봐야 더 깊은 분석 가능

데이터 정제(Data Cleaning)는 단순 제거가 아니라, 비정상값 자체가 비즈니스 인사이트일 수 있다는 점을 확인했다.