고객 여정 어트리뷰션: 멀티 터치포인트 기여도 분석과 마케팅 최적화

이커머스

어트리뷰션고객 여정마케팅 분석멀티터치ROI

이 글은 누구를 위한 것인가

  • "어떤 광고가 진짜 효과가 있는지" 모르는 마케팅 팀
  • 라스트 클릭만 보다가 상위 퍼널 채널이 다 죽은 경험이 있는 팀
  • 광고 예산을 데이터 기반으로 재배분하고 싶은 이커머스 팀

들어가며

고객은 첫 광고를 보고 바로 구매하지 않는다. 인스타그램 광고 → 네이버 검색 → 가격비교 → 이메일 → 구매. 이 여정에서 마지막 클릭(이메일)에만 100% 기여를 주면 인스타그램 광고를 끊어버리는 실수를 한다.

이 글은 bluefoxdev.kr의 마케팅 어트리뷰션 가이드 를 참고하여 작성했습니다.


1. 어트리뷰션 모델 비교

[어트리뷰션 모델 유형]

라스트 클릭 (Last Click):
  기여: 마지막 터치포인트에 100%
  장점: 단순, 구현 쉬움
  단점: 상위 퍼널 채널 과소평가

퍼스트 클릭 (First Click):
  기여: 첫 터치포인트에 100%
  장점: 인지 단계 파악
  단점: 전환 기여 채널 무시

선형 (Linear):
  기여: 모든 터치포인트에 균등 배분
  장점: 공정, 단순
  단점: 실제 기여도 반영 못함

시간 감소 (Time Decay):
  기여: 구매에 가까울수록 높은 가중치
  장점: 전환 기여 채널 강조
  단점: 인지/고려 채널 과소평가

데이터 드리븐 (Data-Driven):
  기여: ML로 실제 기여도 계산
  장점: 가장 정확
  단점: 충분한 데이터 필요 (100+ 전환/채널)

[권장]
  데이터 적을 때: 선형 모델
  데이터 충분할 때: 데이터 드리븐

2. 터치포인트 수집 구현

from datetime import datetime, timedelta
import uuid

class AttributionTracker:
    
    def track_touchpoint(
        self,
        session_id: str,
        user_id: str | None,
        channel: str,        # "organic_search", "paid_search", "instagram", "email"
        campaign: str | None,
        source: str,         # utm_source
        medium: str,         # utm_medium
    ):
        """터치포인트 기록"""
        touchpoint = {
            "id": str(uuid.uuid4()),
            "session_id": session_id,
            "user_id": user_id,
            "channel": channel,
            "campaign": campaign,
            "source": source,
            "medium": medium,
            "timestamp": datetime.utcnow().isoformat(),
        }
        
        # Redis에 30일 보관
        key = f"journey:{user_id or session_id}"
        r.lpush(key, json.dumps(touchpoint))
        r.expire(key, 86400 * 30)
    
    def get_conversion_path(self, user_id: str, order_id: str) -> list[dict]:
        """주문 완료 시 고객 여정 조회"""
        key = f"journey:{user_id}"
        raw = r.lrange(key, 0, -1)  # 최대 50개
        
        touchpoints = [json.loads(tp) for tp in raw]
        touchpoints.reverse()  # 시간 순서로
        
        return touchpoints

def calculate_linear_attribution(touchpoints: list[dict], order_value: float) -> list[dict]:
    """선형 어트리뷰션: 균등 배분"""
    if not touchpoints:
        return []
    
    credit_per_touch = order_value / len(touchpoints)
    
    return [
        {
            "channel": tp["channel"],
            "campaign": tp["campaign"],
            "credit": credit_per_touch,
            "credit_pct": 100 / len(touchpoints),
        }
        for tp in touchpoints
    ]

def calculate_time_decay_attribution(touchpoints: list[dict], order_value: float) -> list[dict]:
    """시간 감소 어트리뷰션: 최근일수록 높은 비중"""
    if not touchpoints:
        return []
    
    # 최근 터치포인트일수록 2배 가중치 (7일 반감기)
    weights = []
    for i, tp in enumerate(touchpoints):
        days_before = len(touchpoints) - i - 1
        weight = 2 ** (-days_before / 7)
        weights.append(weight)
    
    total_weight = sum(weights)
    
    return [
        {
            "channel": tp["channel"],
            "campaign": tp["campaign"],
            "credit": order_value * w / total_weight,
            "credit_pct": w / total_weight * 100,
        }
        for tp, w in zip(touchpoints, weights)
    ]

3. 채널별 ROI 대시보드

def generate_channel_roi_report(
    start_date: str,
    end_date: str,
    model: str = "linear"  # "last_click", "linear", "time_decay"
) -> dict:
    """채널별 기여 수익 및 ROI 계산"""
    
    # 기간 내 완료 주문의 어트리뷰션 데이터 조회
    attributions = db.fetch("""
        SELECT a.channel, a.campaign, 
               SUM(a.credit) as attributed_revenue,
               COUNT(DISTINCT a.order_id) as attributed_orders
        FROM order_attributions a
        JOIN orders o ON o.id = a.order_id
        WHERE o.completed_at BETWEEN $1 AND $2
          AND a.model = $3
        GROUP BY a.channel, a.campaign
    """, start_date, end_date, model)
    
    # 채널별 광고비 조회
    ad_costs = get_ad_costs_by_channel(start_date, end_date)
    
    report = []
    for row in attributions:
        channel = row["channel"]
        cost = ad_costs.get(channel, 0)
        revenue = row["attributed_revenue"]
        roas = revenue / cost if cost > 0 else None
        
        report.append({
            "channel": channel,
            "attributed_revenue": revenue,
            "ad_cost": cost,
            "roas": roas,
            "attributed_orders": row["attributed_orders"],
        })
    
    return sorted(report, key=lambda x: -(x["roas"] or 0))

마무리

어트리뷰션 모델 변경 하나로 마케팅 전략이 완전히 달라질 수 있다. 라스트 클릭에서 선형 모델로 바꾸면 브랜드 광고와 SNS 상위 퍼널의 기여도가 보이기 시작한다. 데이터 드리븐 모델로 가기 전에 선형 모델로 먼저 시작하고, 채널별 기여 수익을 보면서 예산을 재배분하라.