콘텐츠로 이동

VoC 분석


왜 이 분석을?

문제: 지표는 "무엇이" 일어났는지 알려주지만, "왜" 일어났는지는 알려주지 않는다.

VoC(Voice of Customer)는 고객의 목소리에서 정성적 인사이트를 추출한다:

지표: 재구매율 20% 하락
퍼널: 결제 단계 이탈 증가
코호트: 신규 고객 리텐션 저조

→ "왜?"

VoC 분석:
- 리뷰: "배송이 너무 느려요"
- CS: "환불 처리가 복잡해요"
- 설문: "앱이 자주 튕겨요"

→ 원인 파악 + 우선순위 결정

핵심: 숫자가 말해주지 않는 "고객의 맥락"을 이해해야 진짜 문제를 해결할 수 있다.


어떤 가설?

VoC → 가설 전환

VoC 유형 원본 가설 전환 검증 방법
리뷰 "배송 느림" 언급 증가 배송 속도가 재구매율에 영향 배송 소요일별 재구매율 분석
CS "환불 문의" 급증 환불 정책 불만족 환불 사유 분류, 환불 후 재구매율
설문 NPS 하락 특정 경험이 추천 의향 저하 저NPS 고객 심층 인터뷰
SNS 부정 멘션 증가 브랜드 인식 악화 감성 분석, 경쟁사 대비

데이터 소스

수집 채널

소스 특징 수집 방법
앱/웹 리뷰 구매 경험 피드백 크롤링, API
CS 문의 문제점 직접 제기 CRM 데이터, 티켓 시스템
설문 구조화된 피드백 인앱 설문, 이메일
SNS 자연스러운 의견 소셜 리스닝 툴
인터뷰 깊은 맥락 1:1 사용자 인터뷰

데이터 전처리

import pandas as pd
import re

def preprocess_text(text):
    # 소문자 변환
    text = text.lower()
    # 특수문자 제거 (한글, 영문, 숫자만)
    text = re.sub(r'[^가-힣a-z0-9\s]', '', text)
    # 연속 공백 제거
    text = re.sub(r'\s+', ' ', text).strip()
    return text

# 적용
df['clean_text'] = df['review_text'].apply(preprocess_text)

# 불용어 제거 (한국어)
stopwords = ['이', '가', '은', '는', '을', '를', '에', '의', '도', '로', '으로']
df['tokens'] = df['clean_text'].apply(
    lambda x: [w for w in x.split() if w not in stopwords]
)

분석 방법

1. 토픽 모델링

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.decomposition import LatentDirichletAllocation

# TF-IDF 벡터화
vectorizer = TfidfVectorizer(max_features=1000, ngram_range=(1, 2))
tfidf_matrix = vectorizer.fit_transform(df['clean_text'])

# LDA 토픽 모델링
lda = LatentDirichletAllocation(n_components=5, random_state=42)
lda.fit(tfidf_matrix)

# 토픽별 주요 키워드
feature_names = vectorizer.get_feature_names_out()
for idx, topic in enumerate(lda.components_):
    top_words = [feature_names[i] for i in topic.argsort()[-10:]]
    print(f"토픽 {idx}: {', '.join(top_words)}")

# 결과 예시:
# 토픽 0: 배송, 느림, 도착, 일주일, 기다림
# 토픽 1: 환불, 교환, 절차, 복잡, CS
# 토픽 2: 품질, 사진, 다름, 실망, 반품
# 토픽 3: 앱, 오류, 튕김, 느림, 불편
# 토픽 4: 가격, 할인, 쿠폰, 비쌈, 경쟁사

2. 감성 분석

# 한국어 감성 분석 (KoBERT 기반)
from transformers import AutoTokenizer, AutoModelForSequenceClassification
import torch

model_name = "snunlp/KR-FinBert-SC"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSequenceClassification.from_pretrained(model_name)

def get_sentiment(text):
    inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=512)
    outputs = model(**inputs)
    probs = torch.softmax(outputs.logits, dim=1)
    labels = ['negative', 'neutral', 'positive']
    return labels[probs.argmax()]

df['sentiment'] = df['clean_text'].apply(get_sentiment)

# 감성 분포
sentiment_dist = df['sentiment'].value_counts(normalize=True)

3. 키워드 추출 및 분류

# 카테고리별 키워드 사전 정의
categories = {
    '배송': ['배송', '도착', '느림', '빠름', '택배', '배달'],
    '품질': ['품질', '불량', '하자', '사진', '다름', '실망'],
    '가격': ['가격', '비쌈', '할인', '쿠폰', '경쟁사', '저렴'],
    '서비스': ['CS', '상담', '응대', '환불', '교환', '친절'],
    '앱/UX': ['앱', '오류', '튕김', '느림', '불편', '복잡']
}

def categorize_review(text):
    text_lower = text.lower()
    matched = []
    for cat, keywords in categories.items():
        if any(kw in text_lower for kw in keywords):
            matched.append(cat)
    return matched if matched else ['기타']

df['categories'] = df['clean_text'].apply(categorize_review)

# 카테고리별 집계
from collections import Counter
all_cats = [cat for cats in df['categories'] for cat in cats]
category_counts = Counter(all_cats)

비즈니스 액션

인사이트 → 액션 프레임워크

VoC 수집 → 분류 → 정량화 → 우선순위 → 액션 → 검증

우선순위 결정

카테고리 언급 비율 감성 영향 추정 우선순위
배송 35% 부정 78% 재구매율 -15% 1
품질 25% 부정 85% 반품율 +10% 2
앱/UX 20% 부정 60% 이탈률 +5% 3
가격 15% 중립 50% - 4
서비스 5% 부정 40% - 5

실제 적용 예시

문제: NPS가 전분기 대비 10점 하락

VoC 분석:

# 저NPS 고객 (Detractor) 리뷰 분석
detractors = df[df['nps_score'] <= 6]

# 토픽 분포
detractor_topics = detractors['categories'].explode().value_counts()
# 배송: 45%
# 품질: 30%
# 앱/UX: 15%
# 기타: 10%

# 대표 리뷰 추출
배송_reviews = detractors[detractors['categories'].apply(lambda x: '배송' in x)]
print(배송_reviews['review_text'].sample(5).tolist())
# "주문한 지 10일 지났는데 아직도 안 왔어요"
# "배송 추적이 안 돼서 불안했습니다"
# "경쟁사는 다음 날 오는데..."

가설: 배송 속도와 추적 불가가 NPS 하락의 주요 원인

액션: 1. 물류 파트너 협의: 평균 배송일 7일 → 4일 목표 2. 배송 추적 시스템 개선: 실시간 위치 노출 3. 예상 도착일 정확도 개선

검증: - 배송일 4일 이하 고객 NPS vs 5일 이상 고객 NPS 비교 - 개선 후 월간 NPS 추적


정량 데이터와의 결합

VoC + 퍼널

# 이탈 구간별 VoC 매핑
# 장바구니 이탈 고객의 CS 문의 분석
cart_churned = df[df['last_event'] == 'add_cart']
cs_from_churned = cs_data[cs_data['user_id'].isin(cart_churned['user_id'])]

# 문의 유형 분석
cs_topics = cs_from_churned['topic'].value_counts()
# 배송비 문의: 40%
# 재고 문의: 25%
# 쿠폰 문의: 20%

# → 장바구니 이탈 원인: 배송비 충격

VoC + 코호트

# 이탈 코호트의 VoC 특성
churned_cohort = df[(df['cohort'] == '2024-01') & (df['churned'] == True)]
retained_cohort = df[(df['cohort'] == '2024-01') & (df['churned'] == False)]

# 리뷰 감성 비교
churned_sentiment = churned_cohort['sentiment'].value_counts(normalize=True)
retained_sentiment = retained_cohort['sentiment'].value_counts(normalize=True)

# 이탈 고객: 부정 60%, 중립 30%, 긍정 10%
# 유지 고객: 부정 20%, 중립 40%, 긍정 40%

주의사항

함정

  1. 표본 편향
  2. 리뷰 남기는 사람 = 극단적 경험자 (매우 만족 or 매우 불만)
  3. 해결: 무작위 설문 병행, 침묵하는 다수 인지

  4. 해석 편향

  5. 보고 싶은 것만 보는 확증 편향
  6. 해결: 정량화, 여러 명이 독립적으로 분석

  7. 맥락 무시

  8. "비싸다" = 절대 가격? 가성비? 경쟁사 대비?
  9. 해결: 원문 맥락 확인, 후속 인터뷰

  10. 시간 지연

  11. 리뷰는 과거 경험, 현재 상황과 다를 수 있음
  12. 해결: 최신 데이터 우선, 시계열 추세 확인

보완 분석

한계 보완 방법
정량적 영향 모름 지표 분석과 결합
대표성 의문 설문, A/B 테스트로 검증
해결책 효과 불확실 개선 후 VoC 재측정

실용 팁

빠른 인사이트 추출

# 부정 리뷰 워드클라우드
from wordcloud import WordCloud

negative_text = ' '.join(df[df['sentiment'] == 'negative']['clean_text'])
wc = WordCloud(font_path='NanumGothic.ttf', 
               width=800, height=400,
               background_color='white').generate(negative_text)

주간 VoC 리포트 자동화

def weekly_voc_report(df, date_col='created_at'):
    last_week = df[df[date_col] >= pd.Timestamp.now() - pd.Timedelta(days=7)]

    report = {
        '총 VoC 수': len(last_week),
        '감성 분포': last_week['sentiment'].value_counts(normalize=True).to_dict(),
        '주요 토픽': last_week['categories'].explode().value_counts().head(5).to_dict(),
        '긴급 이슈': last_week[last_week['sentiment'] == 'negative']['clean_text'].head(10).tolist()
    }
    return report

경쟁사 비교

# 경쟁사 언급 추출
competitors = ['쿠팡', '네이버', '11번가', 'SSG']

def extract_competitor_mentions(text):
    mentioned = [c for c in competitors if c in text]
    return mentioned if mentioned else None

df['competitor_mention'] = df['clean_text'].apply(extract_competitor_mentions)
competitor_context = df[df['competitor_mention'].notna()]

# "쿠팡은 다음 날 오는데...", "네이버보다 비싸요"