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)
비즈니스 액션¶
인사이트 → 액션 프레임워크¶
우선순위 결정¶
| 카테고리 | 언급 비율 | 감성 | 영향 추정 | 우선순위 |
|---|---|---|---|---|
| 배송 | 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%
주의사항¶
함정¶
- 표본 편향
- 리뷰 남기는 사람 = 극단적 경험자 (매우 만족 or 매우 불만)
-
해결: 무작위 설문 병행, 침묵하는 다수 인지
-
해석 편향
- 보고 싶은 것만 보는 확증 편향
-
해결: 정량화, 여러 명이 독립적으로 분석
-
맥락 무시
- "비싸다" = 절대 가격? 가성비? 경쟁사 대비?
-
해결: 원문 맥락 확인, 후속 인터뷰
-
시간 지연
- 리뷰는 과거 경험, 현재 상황과 다를 수 있음
- 해결: 최신 데이터 우선, 시계열 추세 확인
보완 분석¶
| 한계 | 보완 방법 |
|---|---|
| 정량적 영향 모름 | 지표 분석과 결합 |
| 대표성 의문 | 설문, 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()]
# "쿠팡은 다음 날 오는데...", "네이버보다 비싸요"