콘텐츠로 이동
Data Prep
상세

Market Basket Analysis

연관 규칙 분석을 활용한 장바구니 분석 가이드.

개요

Market Basket Analysis(MBA)는 고객의 구매 패턴에서 함께 구매되는 상품 조합을 발견하는 기법이다.

구분 설명
목적 상품 간 연관 관계 발견
핵심 기법 Association Rule Mining
대표 알고리즘 Apriori, FP-Growth
적용 분야 리테일, 이커머스, 추천 시스템

핵심 개념

주요 지표

지표 정의 해석
Support \(P(A \cap B)\) 함께 구매된 비율
Confidence \(P(B\|A)\) A 구매 시 B 구매 확률
Lift \(\frac{P(B\|A)}{P(B)}\) 기대 대비 실제 연관성
Conviction \(\frac{1-P(B)}{1-Conf(A→B)}\) 규칙의 방향성 강도

수식

Support: $\(Support(A \rightarrow B) = \frac{|A \cap B|}{N}\)$

Confidence: $\(Confidence(A \rightarrow B) = \frac{Support(A \cap B)}{Support(A)}\)$

Lift: $\(Lift(A \rightarrow B) = \frac{Support(A \cap B)}{Support(A) \times Support(B)}\)$

Lift 해석

Lift 값 해석
> 1 양의 연관 (함께 구매 경향)
= 1 독립 (연관 없음)
< 1 음의 연관 (대체 관계)

알고리즘

1. Apriori

원리: 빈발 아이템셋의 부분집합도 빈발하다 (Downward Closure)

┌─────────────────────────────────────────────────────────┐
│                   Apriori 알고리즘                       │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  1단계: 빈발 1-아이템셋                                  │
│  {빵}: 4, {우유}: 5, {버터}: 3, {맥주}: 3              │
│  (min_support = 3 이상)                                 │
│                                                         │
│  2단계: 후보 2-아이템셋 생성                            │
│  {빵,우유}, {빵,버터}, {빵,맥주}, ...                   │
│                                                         │
│  3단계: 빈발 2-아이템셋 필터링                          │
│  {빵,우유}: 3 ✓, {우유,버터}: 3 ✓                       │
│                                                         │
│  4단계: 반복 (k-아이템셋 → k+1-아이템셋)                │
│  ...                                                    │
│                                                         │
│  5단계: 연관 규칙 생성                                  │
│  {빵} → {우유}: conf=0.75, lift=1.2                     │
│                                                         │
└─────────────────────────────────────────────────────────┘

시간 복잡도: \(O(2^n)\) (최악), 실제로는 pruning으로 감소

2. FP-Growth

원리: 데이터베이스 2번 스캔, FP-Tree 구조 활용

┌─────────────────────────────────────────────────────────┐
│                   FP-Tree 구조                          │
├─────────────────────────────────────────────────────────┤
│                                                         │
│                      [Root]                             │
│                      /    \                             │
│                   [우유:5]  [빵:1]                       │
│                   /   \                                 │
│               [빵:3] [버터:2]                            │
│                 |                                       │
│             [버터:1]                                     │
│                                                         │
│  Header Table:                                          │
│  우유 → [Node1]                                         │
│  빵   → [Node2] → [Node3]                               │
│  버터 → [Node4] → [Node5]                               │
│                                                         │
└─────────────────────────────────────────────────────────┘

장점: - DB 스캔 2회 (Apriori는 k회) - 후보 생성 없음 - 대규모 데이터에 효율적

Python 구현

mlxtend 활용

from mlxtend.frequent_patterns import apriori, fpgrowth, association_rules
from mlxtend.preprocessing import TransactionEncoder
import pandas as pd

# 트랜잭션 데이터
transactions = [
    ['빵', '우유', '버터'],
    ['빵', '우유'],
    ['우유', '버터'],
    ['빵', '맥주', '기저귀'],
    ['우유', '맥주', '기저귀'],
]

# 원핫 인코딩
te = TransactionEncoder()
te_array = te.fit(transactions).transform(transactions)
df = pd.DataFrame(te_array, columns=te.columns_)

# 빈발 아이템셋 추출
frequent = fpgrowth(df, min_support=0.3, use_colnames=True)

# 연관 규칙 생성
rules = association_rules(frequent, metric="lift", min_threshold=1.0)

# 상위 규칙 확인
print(rules.sort_values('lift', ascending=False).head(10))

대규모 데이터 처리

from pyspark.ml.fpm import FPGrowth

# Spark FP-Growth
fp = FPGrowth(itemsCol="items", minSupport=0.01, minConfidence=0.5)
model = fp.fit(transactions_df)

# 빈발 아이템셋
model.freqItemsets.show()

# 연관 규칙
model.associationRules.show()

고급 기법

1. 시계열 장바구니 분석

구매 순서를 고려한 시퀀스 패턴:

from mlxtend.frequent_patterns import apriori
from collections import defaultdict

def sequential_patterns(transactions, time_window='7D'):
    """시간 순서 고려 패턴"""
    sequences = defaultdict(list)

    for tx in transactions.sort_values('timestamp'):
        user = tx['user_id']
        sequences[user].append({
            'item': tx['item'],
            'time': tx['timestamp']
        })

    # A 구매 후 B 구매 패턴 추출
    sequential_rules = []
    for user, seq in sequences.items():
        for i, item1 in enumerate(seq):
            for item2 in seq[i+1:]:
                if item2['time'] - item1['time'] <= pd.Timedelta(time_window):
                    sequential_rules.append((item1['item'], item2['item']))

    return Counter(sequential_rules)

2. 멀티레벨 분석

상품 계층 (카테고리 → 서브카테고리 → 상품) 적용:

def multilevel_mba(transactions, hierarchy):
    """
    hierarchy = {
        '코카콜라': ('음료', '탄산'),
        '사이다': ('음료', '탄산'),
        '오렌지주스': ('음료', '주스'),
    }
    """
    results = {}

    # Level 1: 카테고리
    cat_transactions = [[hierarchy[item][0] for item in tx] for tx in transactions]
    results['category'] = fpgrowth(encode(cat_transactions), min_support=0.05)

    # Level 2: 서브카테고리
    subcat_transactions = [[hierarchy[item][1] for item in tx] for tx in transactions]
    results['subcategory'] = fpgrowth(encode(subcat_transactions), min_support=0.03)

    # Level 3: 상품
    results['product'] = fpgrowth(encode(transactions), min_support=0.01)

    return results

3. 조건부 장바구니 분석

특정 조건 (시간대, 고객 세그먼트 등) 별 분석:

def conditional_mba(transactions, condition_col):
    """조건별 MBA"""
    results = {}

    for condition in transactions[condition_col].unique():
        subset = transactions[transactions[condition_col] == condition]

        frequent = fpgrowth(encode(subset['items']), min_support=0.02)
        rules = association_rules(frequent, metric="lift", min_threshold=1.2)

        results[condition] = rules

    return results

# 예: 시간대별 분석
weekday_rules = conditional_mba(transactions, 'day_of_week')

비즈니스 활용

1. 상품 배치

┌─────────────────────────────────────────────────────────┐
│               연관 상품 기반 매장 배치                   │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  Lift > 2 상품군                                        │
│  ┌───────┐  ┌───────┐  ┌───────┐                       │
│  │ 파스타│──│ 토마토│──│올리브유│ → 인접 배치           │
│  └───────┘  └───────┘  └───────┘                       │
│                                                         │
│  Lift < 0.8 상품군                                      │
│  ┌───────┐        ┌───────┐                            │
│  │ 콜라  │   VS   │ 사이다│ → 대체재, 분산 배치         │
│  └───────┘        └───────┘                            │
│                                                         │
│  고Lift + 저Support 상품                                │
│  → 교차 판매 기회 (프로모션 연계)                       │
│                                                         │
└─────────────────────────────────────────────────────────┘

2. 번들 상품

def recommend_bundles(rules, min_lift=2.0, min_support=0.02):
    """번들 상품 추천"""
    bundle_candidates = rules[
        (rules['lift'] >= min_lift) & 
        (rules['support'] >= min_support)
    ].copy()

    # 번들 가격 최적화
    for idx, row in bundle_candidates.iterrows():
        items = list(row['antecedents']) + list(row['consequents'])
        individual_price = sum(get_price(item) for item in items)

        # 번들 할인율 계산 (Lift 기반)
        discount_rate = min(0.2, (row['lift'] - 1) * 0.05)
        bundle_price = individual_price * (1 - discount_rate)

        bundle_candidates.loc[idx, 'bundle_price'] = bundle_price

    return bundle_candidates

3. 추천 시스템

def get_recommendations(cart_items, rules, top_n=5):
    """장바구니 기반 추천"""
    cart_set = frozenset(cart_items)

    recommendations = []
    for idx, rule in rules.iterrows():
        antecedents = rule['antecedents']
        consequents = rule['consequents']

        # 장바구니에 antecedents가 포함되어 있고
        # consequents가 포함되어 있지 않으면 추천
        if antecedents.issubset(cart_set) and not consequents.issubset(cart_set):
            for item in consequents:
                if item not in cart_set:
                    recommendations.append({
                        'item': item,
                        'confidence': rule['confidence'],
                        'lift': rule['lift'],
                        'score': rule['confidence'] * rule['lift']
                    })

    # 중복 제거 및 정렬
    unique_recs = {}
    for rec in recommendations:
        item = rec['item']
        if item not in unique_recs or rec['score'] > unique_recs[item]['score']:
            unique_recs[item] = rec

    return sorted(unique_recs.values(), key=lambda x: x['score'], reverse=True)[:top_n]

4. 프로모션 전략

규칙 패턴 전략
높은 Lift, 낮은 Support 타겟 프로모션 (신규 조합 홍보)
높은 Lift, 높은 Support 번들 할인
낮은 Lift, 높은 Support 개별 마진 유지
A→B 일방향 강함 B를 미끼 상품으로 활용

시각화

네트워크 그래프

import networkx as nx
import matplotlib.pyplot as plt

def plot_association_network(rules, min_lift=1.5):
    """연관 규칙 네트워크 시각화"""
    G = nx.DiGraph()

    for idx, row in rules[rules['lift'] >= min_lift].iterrows():
        for ant in row['antecedents']:
            for con in row['consequents']:
                G.add_edge(ant, con, weight=row['lift'], confidence=row['confidence'])

    pos = nx.spring_layout(G)

    # Edge width = lift, color = confidence
    edges = G.edges(data=True)
    weights = [e[2]['weight'] for e in edges]
    colors = [e[2]['confidence'] for e in edges]

    plt.figure(figsize=(12, 8))
    nx.draw(G, pos, with_labels=True, 
            edge_color=colors, edge_cmap=plt.cm.Reds,
            width=[w/2 for w in weights], 
            node_size=2000, node_color='lightblue')
    plt.title("Association Rules Network")
    plt.show()

히트맵

import seaborn as sns

def plot_lift_heatmap(rules):
    """Lift 히트맵"""
    # Pivot table 생성
    pivot_data = []
    for idx, row in rules.iterrows():
        for ant in row['antecedents']:
            for con in row['consequents']:
                pivot_data.append({
                    'antecedent': ant,
                    'consequent': con,
                    'lift': row['lift']
                })

    pivot_df = pd.DataFrame(pivot_data).pivot(
        index='antecedent', columns='consequent', values='lift'
    )

    plt.figure(figsize=(12, 10))
    sns.heatmap(pivot_df, annot=True, cmap='RdYlGn', center=1)
    plt.title("Association Rules Lift Heatmap")
    plt.show()

실무 고려사항

임계값 설정

지표 권장 범위 비고
min_support 0.01-0.05 희소 데이터는 낮게
min_confidence 0.3-0.6 비즈니스 목적에 따라
min_lift 1.2-2.0 실질적 연관성

데이터 전처리

def preprocess_transactions(df):
    """트랜잭션 전처리"""
    # 1. 반품 제거
    df = df[df['quantity'] > 0]

    # 2. 이상치 제거 (대량 구매)
    df = df[df['quantity'] < df['quantity'].quantile(0.99)]

    # 3. 희소 상품 필터링
    item_counts = df['item'].value_counts()
    valid_items = item_counts[item_counts >= 10].index
    df = df[df['item'].isin(valid_items)]

    # 4. 그룹화
    transactions = df.groupby('transaction_id')['item'].apply(list).tolist()

    return transactions

성능 최적화

문제 해결책
메모리 부족 FP-Growth 사용, 샘플링
너무 많은 규칙 임계값 상향, 상위 N개만
희소 아이템 카테고리 레벨 분석

요약

Market Basket Analysis는 상품 간 연관 관계를 발견하여 상품 배치, 번들링, 추천, 프로모션에 활용한다. Support/Confidence/Lift 지표로 규칙의 품질을 평가하고, FP-Growth 알고리즘으로 대규모 데이터를 효율적으로 처리한다.