🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Network Effects 및 Interference 처리 완벽 가이드 - 슬라이드 1/9
A

AI Generated

2025. 12. 3. · 9 Views

Network Effects 및 Interference 처리 완벽 가이드

데이터 과학에서 네트워크 효과와 간섭 현상을 이해하고 처리하는 방법을 알아봅니다. A/B 테스트의 함정부터 인과추론의 핵심 개념까지 실무 예제와 함께 설명합니다.


목차

  1. SUTVA와_간섭_문제_이해하기
  2. 네트워크_클러스터_무작위_배정
  3. 직접_효과와_간접_효과_분리하기
  4. 경쟁_효과와_보완_효과
  5. 노출_매핑과_간접_처치_측정
  6. 이중_무작위_설계
  7. 시간에_따른_네트워크_효과_확산
  8. 인과_그래프를_활용한_간섭_모델링

1. SUTVA와 간섭 문제 이해하기

김개발 씨는 데이터 분석팀에 입사한 지 6개월이 되었습니다. 어느 날 마케팅팀에서 "새로운 할인 쿠폰의 효과를 측정해달라"는 요청이 들어왔습니다.

간단한 A/B 테스트라고 생각했는데, 결과가 이상하게 나왔습니다. 쿠폰을 받지 않은 그룹의 구매율도 함께 올라간 것입니다.

**SUTVA(Stable Unit Treatment Value Assumption)**는 "한 사람에게 적용한 처치가 다른 사람에게 영향을 주지 않는다"는 가정입니다. 마치 교실에서 한 학생에게만 간식을 주었는데, 옆 친구들이 부러워하며 행동이 달라지는 것과 같습니다.

이 가정이 깨지면 우리가 측정한 효과는 실제와 다를 수 있습니다.

다음 코드를 살펴봅시다.

import numpy as np
import pandas as pd

# SUTVA 위반 시뮬레이션: 친구 효과가 있는 경우
np.random.seed(42)
n_users = 1000

# 사용자와 친구 관계 생성
users = pd.DataFrame({
    'user_id': range(n_users),
    'treated': np.random.binomial(1, 0.5, n_users)  # 50%가 처치군
})

# 간섭 효과: 친구가 쿠폰을 받으면 나도 영향받음
def calculate_outcome_with_interference(row, users_df, interference_strength=0.3):
    direct_effect = 0.2 if row['treated'] else 0
    # 친구 중 처치받은 비율에 따른 간접 효과
    friend_treated_ratio = users_df['treated'].mean()
    indirect_effect = interference_strength * friend_treated_ratio
    return direct_effect + indirect_effect + np.random.normal(0, 0.1)

users['outcome'] = users.apply(
    lambda row: calculate_outcome_with_interference(row, users), axis=1
)

김개발 씨는 입사 6개월 차 데이터 분석가입니다. 오늘도 열심히 A/B 테스트 결과를 분석하던 중, 이상한 현상을 발견했습니다.

분명히 쿠폰을 받지 않은 대조군인데, 왜 구매율이 평소보다 높게 나온 걸까요? 선배 분석가 박시니어 씨가 다가와 데이터를 살펴봅니다.

"아, 이건 SUTVA 위반 때문이에요. 네트워크 효과를 고려하지 않아서 생긴 문제입니다." 그렇다면 SUTVA란 정확히 무엇일까요?

쉽게 비유하자면, SUTVA는 마치 예방접종과 같습니다. 내가 독감 백신을 맞았다고 해서 옆 사람의 건강 상태가 바뀌지 않는다고 가정하는 것입니다.

하지만 현실에서는 내가 백신을 맞으면 옆 사람에게 독감을 옮길 확률이 줄어들어, 결과적으로 옆 사람도 영향을 받게 됩니다. SUTVA가 없던 시절, 아니 정확히 말하면 SUTVA를 무시하던 시절에는 어땠을까요?

분석가들은 단순히 처치군과 대조군의 평균을 비교했습니다. "쿠폰을 받은 사람의 구매율은 30%, 받지 않은 사람은 20%이니 쿠폰 효과는 10%입니다." 이렇게 결론을 내렸습니다.

하지만 만약 쿠폰을 받은 친구가 "나 이거 싸게 샀어!"라고 SNS에 올리면 어떻게 될까요? 쿠폰을 받지 않은 사람들도 그 상품에 관심을 갖게 됩니다.

바로 이런 문제를 인식하기 위해 SUTVA 개념이 중요해졌습니다. **SUTVA(Stable Unit Treatment Value Assumption)**는 두 가지 핵심 가정을 담고 있습니다.

첫째, 한 단위에 대한 처치가 다른 단위의 결과에 영향을 주지 않습니다. 둘째, 처치의 형태가 단일해야 합니다.

이 가정이 성립해야만 우리가 측정한 처치 효과가 순수한 효과라고 말할 수 있습니다. 위의 코드를 살펴보겠습니다.

먼저 1000명의 사용자를 생성하고 50%를 무작위로 처치군에 배정합니다. 핵심은 calculate_outcome_with_interference 함수입니다.

이 함수에서 결과값은 **직접 효과(direct_effect)**와 **간접 효과(indirect_effect)**의 합으로 계산됩니다. 간접 효과는 전체 사용자 중 처치받은 비율에 비례하여 모든 사용자에게 영향을 미칩니다.

실제 현업에서는 어떻게 활용할까요? SNS 플랫폼을 운영한다고 가정해봅시다.

새로운 기능을 일부 사용자에게만 먼저 공개했는데, 그 사용자들이 친구들에게 "이 기능 좋아!"라고 공유합니다. 그러면 기능을 받지 않은 사용자들의 행동도 달라집니다.

기대감이 높아지거나, 혹은 소외감을 느낄 수도 있습니다. 이런 상황에서 단순 비교는 실제 효과를 왜곡합니다.

하지만 주의할 점도 있습니다. 초보 분석가들이 흔히 하는 실수는 "우리 서비스는 사용자끼리 연결되어 있지 않으니 SUTVA가 성립한다"고 가정하는 것입니다.

하지만 같은 지역에 살거나, 같은 회사에 다니거나, 온라인 커뮤니티를 공유하는 것만으로도 간섭이 발생할 수 있습니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다. "아, 그래서 대조군의 구매율도 올라갔군요!

처치군이 주변에 입소문을 냈으니까요." SUTVA를 이해하면 A/B 테스트의 한계를 인식하고, 더 정확한 분석 방법을 설계할 수 있습니다.

실전 팁

💡 - A/B 테스트 설계 시 사용자 간 연결 관계를 먼저 파악하세요

  • 네트워크 데이터가 있다면 클러스터 단위 무작위 배정을 고려하세요
  • SUTVA 위반이 의심되면 효과 크기를 과대 또는 과소 추정했을 가능성을 보고서에 명시하세요

2. 네트워크 클러스터 무작위 배정

김개발 씨는 SUTVA 문제를 해결하기 위해 고민에 빠졌습니다. 개인 단위로 무작위 배정하면 간섭이 발생하는데, 어떻게 해야 할까요?

박시니어 씨가 화이트보드에 네트워크 그림을 그리며 설명을 시작했습니다. "클러스터 단위로 배정하면 됩니다."

**클러스터 무작위 배정(Cluster Randomization)**은 개인이 아닌 그룹 단위로 처치를 배정하는 방법입니다. 마치 학급 전체에 같은 교수법을 적용하는 것처럼, 연결된 사람들을 하나의 단위로 묶어 처리합니다.

이렇게 하면 클러스터 내부의 간섭은 허용하되, 클러스터 간 간섭을 차단할 수 있습니다.

다음 코드를 살펴봅시다.

import networkx as nx
from sklearn.cluster import SpectralClustering

# 소셜 네트워크 생성
G = nx.barabasi_albert_graph(500, 3, seed=42)

# 네트워크를 클러스터로 분할
adjacency_matrix = nx.to_numpy_array(G)
n_clusters = 10

clustering = SpectralClustering(
    n_clusters=n_clusters,
    affinity='precomputed',
    random_state=42
)
cluster_labels = clustering.fit_predict(adjacency_matrix + np.eye(len(adjacency_matrix)))

# 클러스터 단위로 처치 배정
np.random.seed(42)
treated_clusters = np.random.choice(n_clusters, size=n_clusters//2, replace=False)

# 각 노드의 처치 여부 결정
node_treatment = np.array([
    1 if cluster_labels[i] in treated_clusters else 0
    for i in range(len(G.nodes()))
])

print(f"처치 클러스터: {treated_clusters}")
print(f"처치군 노드 수: {sum(node_treatment)}, 대조군 노드 수: {len(node_treatment) - sum(node_treatment)}")

김개발 씨는 SUTVA 위반 문제를 어떻게 해결할지 며칠째 고민 중이었습니다. 개인 단위 A/B 테스트가 문제라면, 아예 실험을 하지 말아야 하는 걸까요?

박시니어 씨가 회의실 화이트보드 앞으로 걸어갔습니다. "김개발 씨, 문제를 다른 각도에서 바라보면 해답이 보입니다." 그렇다면 클러스터 무작위 배정이란 무엇일까요?

쉽게 비유하자면, 이것은 마치 학교에서 교육 프로그램을 테스트하는 것과 같습니다. 같은 반 학생들은 서로 영향을 주고받으니, 한 반 전체에 새 교수법을 적용하고 다른 반 전체에는 기존 교수법을 유지합니다.

반 내부에서는 학생들이 서로 영향을 줘도 괜찮습니다. 중요한 것은 다른 반에는 영향이 가지 않도록 하는 것입니다.

개인 단위 배정이 문제가 되는 이유를 다시 생각해봅시다. 친한 친구 다섯 명이 있다고 가정합니다.

이 중 세 명이 처치군, 두 명이 대조군에 배정되었습니다. 처치군 친구들이 "이 기능 진짜 좋아!"라고 이야기하면, 대조군 친구들도 기대감을 갖거나 심지어 우회 방법을 찾으려 할 수 있습니다.

결국 순수한 대조군이 존재하지 않게 됩니다. 바로 이런 문제를 해결하기 위해 클러스터 단위 배정이 등장했습니다.

클러스터 무작위 배정에서는 먼저 네트워크를 여러 클러스터로 나눕니다. 클러스터 내부에는 많은 연결이 있고, 클러스터 사이에는 연결이 적도록 분할합니다.

그다음 클러스터 전체를 처치군 또는 대조군으로 배정합니다. 이렇게 하면 클러스터 간 간섭이 최소화됩니다.

위의 코드를 단계별로 살펴보겠습니다. 먼저 nx.barabasi_albert_graph로 현실적인 소셜 네트워크를 생성합니다.

이 모델은 "부익부 빈익빈" 현상을 반영하여 일부 노드가 많은 연결을 갖는 구조를 만듭니다. 다음으로 스펙트럴 클러스터링을 사용하여 네트워크를 10개 그룹으로 나눕니다.

마지막으로 클러스터 중 절반을 무작위로 선택하여 처치군으로 지정합니다. 실제 현업에서는 어떻게 활용할까요?

배달 앱 서비스를 운영한다고 가정해봅시다. 같은 아파트 단지에 사는 사람들은 서로 음식점 추천을 주고받습니다.

새로운 추천 알고리즘을 테스트할 때, 한 단지에는 새 알고리즘을, 다른 단지에는 기존 알고리즘을 적용합니다. 이렇게 하면 단지 내 입소문 효과는 그대로 두면서도 클러스터 간 비교가 가능해집니다.

하지만 주의할 점도 있습니다. 클러스터 무작위 배정의 가장 큰 단점은 **통계적 검정력(statistical power)**이 낮아진다는 것입니다.

개인 1000명 대신 클러스터 10개를 비교하는 셈이니, 효과를 감지하기가 더 어려워집니다. 따라서 더 많은 클러스터가 필요하거나 더 큰 효과만 감지할 수 있습니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨가 물었습니다.

"그러면 클러스터를 어떻게 나누는 게 좋을까요?" 박시니어 씨가 미소를 지었습니다. "그건 도메인 지식과 네트워크 구조를 함께 고려해야 해요." 클러스터 무작위 배정을 이해하면 네트워크 효과가 있는 상황에서도 신뢰할 수 있는 실험을 설계할 수 있습니다.

실전 팁

💡 - 클러스터는 내부 연결은 많고 외부 연결은 적도록 나누세요

  • 검정력 계산 시 클러스터 수와 클러스터 내 상관관계를 반드시 고려하세요
  • 지리적 클러스터(도시, 지역)는 자연스러운 분할 기준이 될 수 있습니다

3. 직접 효과와 간접 효과 분리하기

김개발 씨는 클러스터 실험을 성공적으로 진행했습니다. 그런데 마케팅팀에서 또 다른 질문이 들어왔습니다.

"쿠폰의 직접 효과와 입소문을 통한 간접 효과를 따로 알고 싶어요." 이번에는 효과를 분리해야 하는 새로운 도전이 시작되었습니다.

**직접 효과(Direct Effect)**는 처치를 직접 받아서 생기는 효과이고, 간접 효과(Indirect Effect) 또는 **스필오버 효과(Spillover Effect)**는 주변 사람이 처치를 받아서 나에게 전파되는 효과입니다. 마치 백신의 직접 보호 효과와 집단 면역 효과를 분리하는 것과 같습니다.

다음 코드를 살펴봅시다.

import numpy as np
import pandas as pd
from scipy import stats

# 2x2 설계: 개인 처치 여부 x 클러스터 처치 여부
np.random.seed(42)
n_clusters = 20
n_per_cluster = 50

data = []
for cluster_id in range(n_clusters):
    # 클러스터 수준 처치 (50% 처치 클러스터)
    cluster_treated = cluster_id < n_clusters // 2

    for i in range(n_per_cluster):
        # 개인 수준 처치 (처치 클러스터 내에서 70%가 개인 처치)
        if cluster_treated:
            individual_treated = np.random.random() < 0.7
        else:
            individual_treated = np.random.random() < 0.1  # 대조 클러스터에서 10%만 처치

        # 결과 생성: 직접 효과(0.3) + 간접 효과(0.15)
        direct_effect = 0.3 if individual_treated else 0
        indirect_effect = 0.15 if cluster_treated else 0
        outcome = direct_effect + indirect_effect + np.random.normal(0, 0.2)

        data.append({
            'cluster_id': cluster_id,
            'cluster_treated': cluster_treated,
            'individual_treated': individual_treated,
            'outcome': outcome
        })

df = pd.DataFrame(data)

# 효과 분리 추정
print("직접 효과 추정:", df[df['individual_treated']]['outcome'].mean() -
      df[(~df['individual_treated']) & (df['cluster_treated'])]['outcome'].mean())
print("간접 효과 추정:", df[(~df['individual_treated']) & (df['cluster_treated'])]['outcome'].mean() -
      df[~df['cluster_treated']]['outcome'].mean())

김개발 씨는 이제 네트워크 효과의 존재를 인식하고 있었습니다. 하지만 마케팅팀의 새로운 요청은 한 단계 더 어려웠습니다.

"쿠폰을 직접 받아서 구매한 건지, 친구가 쿠폰 받은 걸 보고 구매한 건지 구분해주세요." 박시니어 씨가 노트북을 열며 말했습니다. "이건 2x2 실험 설계가 필요한 상황이에요." 그렇다면 직접 효과와 간접 효과는 어떻게 다른 걸까요?

쉽게 비유하자면, 이것은 마치 유행하는 식당과 같습니다. 직접 효과는 내가 그 식당 광고를 보고 방문하는 것입니다.

간접 효과는 친구가 그 식당에 갔다 와서 "거기 맛있더라"라고 말해서 내가 가게 되는 것입니다. 둘 다 결과는 "식당 방문"이지만, 경로가 다릅니다.

이 둘을 왜 구분해야 할까요? 마케팅 전략이 완전히 달라지기 때문입니다.

만약 직접 효과가 크다면 광고 예산을 더 투입해야 합니다. 반면 간접 효과가 크다면 바이럴 마케팅이나 추천 프로그램에 집중하는 것이 효율적입니다.

효과의 원천을 알아야 올바른 투자 결정을 내릴 수 있습니다. 바로 이런 분석을 가능하게 하는 것이 2x2 실험 설계입니다.

이 설계에서는 두 가지 차원을 동시에 조작합니다. 첫째, 클러스터 수준에서 일부 클러스터는 "처치 환경"으로, 나머지는 "대조 환경"으로 지정합니다.

둘째, 개인 수준에서 처치 환경 내에서도 일부만 실제 처치를 받습니다. 이렇게 하면 네 가지 그룹이 만들어집니다.

위의 코드를 자세히 살펴보겠습니다. 먼저 20개 클러스터를 만들고 절반을 처치 클러스터로 지정합니다.

처치 클러스터 내에서는 70%가 개인 처치를 받고, 대조 클러스터에서는 10%만 받습니다. 결과값은 직접 효과 0.3과 간접 효과 0.15를 더해 생성합니다.

이렇게 설계하면 "처치 클러스터 내 미처치 개인"과 "대조 클러스터 내 미처치 개인"을 비교하여 순수한 간접 효과를 추정할 수 있습니다. 실제 현업에서는 어떻게 활용할까요?

라이드셰어링 서비스를 생각해봅시다. 일부 도시에서만 운전자 보너스 프로그램을 시행합니다.

그 도시 내에서도 일부 운전자만 보너스를 받습니다. 보너스를 받지 않은 운전자도 보너스 받은 동료들 덕분에 서비스 품질이 올라간 환경에서 일하게 됩니다.

이 간접 효과를 측정하면 보너스 프로그램의 진정한 가치를 알 수 있습니다. 하지만 주의할 점도 있습니다.

이 설계가 작동하려면 처치 환경 내 미처치 개인이 존재해야 합니다. 하지만 윤리적으로 문제가 될 수 있습니다.

"왜 나만 쿠폰 안 줘요?"라는 불만이 나올 수 있습니다. 또한 처치 클러스터 내에서 개인을 무작위 배정하는 것이 기술적으로 어려울 수도 있습니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 분석 결과를 본 마케팅팀은 놀라워했습니다.

"간접 효과가 직접 효과의 절반이나 되네요! 입소문 전략을 강화해야겠어요." 직접 효과와 간접 효과를 분리할 수 있으면 마케팅 ROI를 더 정확하게 계산하고 최적의 전략을 수립할 수 있습니다.

실전 팁

💡 - 2x2 설계 시 각 셀에 충분한 샘플이 있는지 사전에 계산하세요

  • 간접 효과 추정 시 처치 클러스터와 대조 클러스터의 동질성을 확인하세요
  • 직접 효과와 간접 효과 간 상호작용도 검토하세요

4. 경쟁 효과와 보완 효과

김개발 씨는 이제 네트워크 효과 분석의 전문가가 되어가고 있었습니다. 어느 날 프로덕트팀에서 흥미로운 데이터를 가져왔습니다.

"이상해요. 쿠폰을 많이 뿌린 지역에서 오히려 전체 매출이 줄었어요." 간접 효과가 항상 양수가 아닐 수도 있다는 것을 깨닫는 순간이었습니다.

네트워크 효과는 **보완 효과(Complementary Effect)**일 수도 있고 **경쟁 효과(Competitive Effect)**일 수도 있습니다. 보완 효과는 한 사람의 사용이 다른 사람에게도 이익이 되는 경우이고, 경쟁 효과는 한 사람의 이득이 다른 사람의 손해로 이어지는 경우입니다.

마치 SNS 사용자가 늘면 모두에게 좋지만, 할인 쿠폰이 너무 많으면 희소성이 사라지는 것과 같습니다.

다음 코드를 살펴봅시다.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# 경쟁 효과와 보완 효과 시뮬레이션
np.random.seed(42)
n_markets = 100
n_users_per_market = 50

def simulate_market(n_users, treatment_rate, effect_type='complementary'):
    treated = np.random.random(n_users) < treatment_rate
    n_treated = sum(treated)
    treatment_density = n_treated / n_users

    outcomes = []
    for i in range(n_users):
        direct = 0.3 if treated[i] else 0

        # 보완 효과: 처치 밀도가 높을수록 모두에게 이익
        if effect_type == 'complementary':
            spillover = 0.2 * treatment_density
        # 경쟁 효과: 처치 밀도가 높을수록 처치군의 이점 감소
        else:
            spillover = -0.3 * treatment_density if treated[i] else 0.1 * treatment_density

        outcomes.append(direct + spillover + np.random.normal(0, 0.1))

    return np.mean(outcomes), treatment_density

# 다양한 처치 밀도에서 시장 결과 비교
results_comp = [simulate_market(50, rate, 'complementary') for rate in np.linspace(0.1, 0.9, 20)]
results_compet = [simulate_market(50, rate, 'competitive') for rate in np.linspace(0.1, 0.9, 20)]

print("보완 효과 - 처치율 10%:", round(results_comp[0][0], 3), "처치율 90%:", round(results_comp[-1][0], 3))
print("경쟁 효과 - 처치율 10%:", round(results_compet[0][0], 3), "처치율 90%:", round(results_compet[-1][0], 3))

김개발 씨는 데이터를 들여다보며 고개를 갸웃거렸습니다. 할인 쿠폰을 더 많이 뿌렸는데 왜 효과가 줄어드는 걸까요?

분명히 앞서 배운 간접 효과는 양수였는데 말입니다. 박시니어 씨가 커피를 한 모금 마시며 설명을 시작했습니다.

"간접 효과가 항상 긍정적인 건 아니에요. 경쟁 효과라는 게 있거든요." 그렇다면 경쟁 효과와 보완 효과는 무엇이 다를까요?

쉽게 비유하자면, 보완 효과는 마치 전화기와 같습니다. 전화기를 가진 사람이 늘어날수록 전화기의 가치는 올라갑니다.

통화할 상대가 많아지니까요. 반면 경쟁 효과는 마치 한정판 운동화와 같습니다.

모두가 그 운동화를 신으면 더 이상 특별하지 않습니다. 희소성이 사라지면 가치도 줄어듭니다.

비즈니스 상황에서 이 둘은 어떻게 나타날까요? 보완 효과의 예시를 생각해봅시다.

화상회의 앱을 사용하는 동료가 많아질수록 나도 그 앱을 쓸 유인이 커집니다. 언어 학습 앱에서 학습 파트너가 많아지면 모두의 학습 효과가 올라갑니다.

이런 경우 처치율을 높이면 전체 효과도 커집니다. 경쟁 효과는 다릅니다.

특가 할인 쿠폰을 모든 고객에게 주면 어떻게 될까요? "나만 받은 특별한 혜택"이라는 느낌이 사라집니다.

채용 지원서 첨삭 서비스를 모든 지원자가 이용하면, 아무도 상대적 이점을 갖지 못하게 됩니다. 이런 경우 처치율을 높이면 오히려 개인당 효과가 줄어듭니다.

위의 코드를 살펴보겠습니다. simulate_market 함수에서 핵심은 spillover 계산 부분입니다.

보완 효과에서는 처치 밀도가 높을수록 모든 사람의 결과가 좋아집니다. 경쟁 효과에서는 처치 밀도가 높을수록 처치받은 사람의 추가 이득이 줄어들고, 처치받지 않은 사람은 약간의 이득을 봅니다.

시뮬레이션 결과를 보면 보완 효과에서는 처치율 90%일 때 결과가 가장 좋지만, 경쟁 효과에서는 중간 처치율에서 전체 효과가 최대화됩니다. 실제 현업에서는 어떻게 활용할까요?

추천 시스템을 개인화하는 기능을 출시한다고 가정해봅시다. 초기에는 이 기능을 받은 사용자들이 경쟁자보다 앞서가는 느낌을 받습니다.

하지만 모든 사용자가 이 기능을 쓰게 되면 평준화됩니다. 이런 상황에서는 기능 출시 초기의 효과를 전체 출시 후의 효과로 일반화하면 안 됩니다.

하지만 주의할 점도 있습니다. 같은 서비스에서도 기능에 따라 경쟁 효과와 보완 효과가 다를 수 있습니다.

또한 시간에 따라 효과 유형이 바뀔 수도 있습니다. 초기에는 보완 효과였다가 포화 상태가 되면 경쟁 효과로 전환되기도 합니다.

따라서 지속적인 모니터링이 필요합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

"아하, 쿠폰이 너무 흔해지니까 특별함이 사라진 거군요!" 김개발 씨가 깨달음을 얻었습니다. 박시니어 씨가 고개를 끄덕였습니다.

"그래서 최적 처치율을 찾는 게 중요해요." 경쟁 효과와 보완 효과를 구분할 수 있으면 처치 범위에 대한 전략적 결정을 내릴 수 있습니다.

실전 팁

💡 - 처치 밀도에 따른 효과 변화를 그래프로 시각화하세요

  • 작은 규모 테스트에서 효과가 컸다면 전체 출시 후 감소할 가능성을 고려하세요
  • 경쟁 효과가 있다면 세그먼트별 차등 적용을 검토하세요

5. 노출 매핑과 간접 처치 측정

김개발 씨는 점점 더 정교한 분석을 요구받게 되었습니다. "단순히 클러스터에 속했다는 것만으로는 부족해요.

실제로 처치받은 친구가 몇 명인지에 따라 효과가 다를 거예요." 이번에는 노출 정도를 정밀하게 측정하는 방법을 배워야 했습니다.

**노출 매핑(Exposure Mapping)**은 각 개인이 네트워크를 통해 처치에 얼마나 노출되었는지를 수치화하는 방법입니다. 단순히 "클러스터가 처치됨/안됨"이 아니라 "친구 중 몇 퍼센트가 처치받았는지"를 측정합니다.

마치 간접흡연의 정도가 주변 흡연자 수에 따라 달라지는 것과 같습니다.

다음 코드를 살펴봅시다.

import numpy as np
import pandas as pd
import networkx as nx

# 네트워크 생성
np.random.seed(42)
G = nx.watts_strogatz_graph(200, 4, 0.3)

# 30%를 처치군으로 무작위 배정
nodes = list(G.nodes())
treated_nodes = set(np.random.choice(nodes, size=int(len(nodes)*0.3), replace=False))

# 각 노드의 노출 수준 계산
def calculate_exposure(node, graph, treated_set):
    neighbors = list(graph.neighbors(node))
    if len(neighbors) == 0:
        return 0
    treated_neighbors = sum(1 for n in neighbors if n in treated_set)
    return treated_neighbors / len(neighbors)

exposure_data = []
for node in G.nodes():
    is_treated = node in treated_nodes
    exposure = calculate_exposure(node, G, treated_nodes)

    # 결과 생성: 직접 효과 + 노출 비례 간접 효과
    direct_effect = 0.4 if is_treated else 0
    indirect_effect = 0.5 * exposure  # 노출 비율에 비례
    outcome = direct_effect + indirect_effect + np.random.normal(0, 0.15)

    exposure_data.append({
        'node': node,
        'treated': is_treated,
        'exposure': round(exposure, 2),
        'outcome': round(outcome, 3)
    })

df = pd.DataFrame(exposure_data)
print(df.groupby(['treated', pd.cut(df['exposure'], bins=[0, 0.25, 0.5, 0.75, 1.0])])['outcome'].mean())

김개발 씨는 이제 네트워크 효과 분석에 자신감이 붙었습니다. 하지만 데이터 사이언스팀 리더가 새로운 과제를 던졌습니다.

"클러스터 수준보다 더 정밀하게 측정할 수 있어요. 각 사람마다 노출 정도가 다르잖아요." 박시니어 씨가 네트워크 그림을 가리키며 설명했습니다.

"같은 처치 클러스터에 있어도 친구가 많은 사람과 적은 사람의 노출은 다르죠." 그렇다면 노출 매핑이란 정확히 무엇일까요? 쉽게 비유하자면, 이것은 마치 감염병 역학 조사와 같습니다.

단순히 "확진자가 있는 지역에 산다"보다 "확진자와 몇 번 접촉했는지"가 더 중요한 정보입니다. 밀접 접촉자가 많을수록 감염 위험이 높아지듯, 처치받은 친구가 많을수록 간접 효과도 커집니다.

왜 단순 클러스터 구분으로는 부족할까요? 같은 학교에 다녀도 인싸와 아싸의 사회적 노출은 다릅니다.

친구가 50명인 사람은 그중 10명이 처치받으면 노출률이 20%입니다. 친구가 5명인 사람은 그중 2명만 처치받아도 노출률이 40%입니다.

이 차이를 무시하면 간접 효과를 제대로 추정할 수 없습니다. 바로 이런 정밀한 측정을 가능하게 하는 것이 노출 매핑입니다.

노출 매핑에서는 각 개인에 대해 "네트워크 이웃 중 처치받은 비율"을 계산합니다. 이 값을 **노출 변수(exposure variable)**라고 합니다.

그런 다음 이 노출 변수를 회귀 분석에 포함시켜 간접 효과를 추정합니다. 노출 변수의 계수가 바로 간접 효과의 크기입니다.

위의 코드를 단계별로 살펴보겠습니다. 먼저 Watts-Strogatz 모델로 "작은 세상" 네트워크를 생성합니다.

이 모델은 현실의 사회 네트워크처럼 군집성과 짧은 경로를 모두 갖습니다. calculate_exposure 함수는 각 노드의 이웃 중 처치받은 비율을 계산합니다.

결과 변수는 직접 효과 0.4에 노출 비율의 0.5배를 더해 생성됩니다. 마지막으로 처치 여부와 노출 구간별로 평균 결과를 비교합니다.

실제 현업에서는 어떻게 활용할까요? 메신저 앱에서 새로운 이모티콘 팩을 출시한다고 가정해봅시다.

일부 사용자에게만 먼저 제공합니다. 하지만 이 사용자들이 이모티콘을 사용하면 대화 상대방도 노출됩니다.

대화 상대 중 새 이모티콘 사용자가 많을수록 간접 노출이 강해집니다. 이 노출 수준에 따라 유료 구매 전환율이 어떻게 달라지는지 분석할 수 있습니다.

하지만 주의할 점도 있습니다. 노출 변수는 무작위 배정의 결과이지만, 네트워크 구조 자체는 무작위가 아닙니다.

친구가 많은 사람은 다른 특성도 다를 수 있습니다. 따라서 노출 변수와 결과 사이에 **교란 변수(confounding variable)**가 있을 수 있습니다.

이를 통제하기 위해 네트워크 위치 관련 변수를 함께 포함시켜야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

분석 결과를 보며 김개발 씨가 말했습니다. "노출률이 50% 이상인 사람들은 효과가 두 배나 크네요!" 팀 리더가 만족스러워하며 말했습니다.

"이제 타겟팅을 더 정교하게 할 수 있겠어요." 노출 매핑을 활용하면 네트워크 내 개인 수준의 간접 효과를 정밀하게 측정하고 예측할 수 있습니다.

실전 팁

💡 - 연결 수(degree)와 노출률 사이의 상관관계를 확인하고 필요시 통제하세요

  • 1차 이웃뿐 아니라 2차 이웃까지 확장한 노출 변수도 검토해보세요
  • 노출 효과의 비선형성(체감 효과, 임계점 등)도 탐색하세요

6. 이중 무작위 설계

김개발 씨는 한 가지 근본적인 고민에 빠졌습니다. "노출 변수가 네트워크 구조에 영향받는 게 문제라면, 노출 자체를 무작위로 만들 수는 없을까요?" 박시니어 씨가 미소를 지었습니다.

"좋은 질문이에요. 그게 바로 이중 무작위 설계입니다."

**이중 무작위 설계(Two-Stage Randomization)**는 두 단계로 나누어 무작위 배정을 수행합니다. 1단계에서 클러스터를 처치 강도별로 배정하고, 2단계에서 각 클러스터 내 개인을 처치/대조로 배정합니다.

마치 학교를 선정한 뒤 각 학교 내에서 학생을 다시 선정하는 것과 같습니다.

다음 코드를 살펴봅시다.

import numpy as np
import pandas as pd
from itertools import product

np.random.seed(42)

# 1단계: 클러스터를 처치 강도별로 배정
n_clusters = 30
saturation_levels = [0.0, 0.25, 0.50, 0.75, 1.0]  # 처치 포화도
clusters_per_level = n_clusters // len(saturation_levels)

cluster_assignments = []
for i, sat in enumerate(saturation_levels):
    cluster_assignments.extend([sat] * clusters_per_level)

np.random.shuffle(cluster_assignments)

# 2단계: 클러스터 내 개인 배정 및 결과 생성
n_per_cluster = 40
data = []

for cluster_id, saturation in enumerate(cluster_assignments):
    n_treated = int(n_per_cluster * saturation)
    treatment_status = [1]*n_treated + [0]*(n_per_cluster - n_treated)
    np.random.shuffle(treatment_status)

    for i, treated in enumerate(treatment_status):
        # 결과: 직접 효과 + 포화도 기반 간접 효과
        direct = 0.3 * treated
        spillover = 0.4 * saturation
        outcome = direct + spillover + np.random.normal(0, 0.15)

        data.append({
            'cluster_id': cluster_id,
            'saturation': saturation,
            'treated': treated,
            'outcome': outcome
        })

df = pd.DataFrame(data)

# 포화도별 평균 효과 비교
print("포화도별 평균 결과:")
print(df.groupby('saturation')['outcome'].mean().round(3))
print("\n포화도별 처치 효과 (처치군 - 대조군):")
for sat in saturation_levels[1:]:  # 0 제외
    treated_mean = df[(df['saturation']==sat) & (df['treated']==1)]['outcome'].mean()
    control_mean = df[(df['saturation']==sat) & (df['treated']==0)]['outcome'].mean()
    print(f"포화도 {sat}: {round(treated_mean - control_mean, 3)}")

김개발 씨는 앞서 배운 내용을 정리하다가 문득 의문이 들었습니다. 노출 매핑에서 노출 변수는 결국 네트워크 구조와 개인 특성의 영향을 받습니다.

이것을 완전히 통제할 방법은 없을까요? 박시니어 씨가 화이트보드에 새로운 그림을 그리기 시작했습니다.

"가장 깔끔한 해결책은 포화도(saturation) 자체를 실험 변수로 만드는 거예요." 그렇다면 이중 무작위 설계란 무엇일까요? 쉽게 비유하자면, 이것은 마치 토너먼트 대회 조 편성과 같습니다.

먼저 전체 팀을 A조, B조, C조로 나눕니다. 그다음 각 조 내에서 시드 배정을 합니다.

두 단계 모두 무작위로 진행되어야 공정한 대회가 됩니다. 이중 무작위 설계도 마찬가지로 두 단계의 무작위 배정을 수행합니다.

기존 방식과 무엇이 다른 걸까요? 기존 클러스터 무작위 배정에서는 처치 클러스터와 대조 클러스터만 있었습니다.

처치 클러스터 내에서는 대부분이 처치를 받았습니다. 이중 무작위 설계에서는 클러스터별로 처치 포화도가 다릅니다.

어떤 클러스터는 25%만 처치, 어떤 클러스터는 75% 처치, 이렇게 다양한 수준을 실험적으로 조작합니다. 바로 이 설계가 강력한 이유가 있습니다.

포화도를 무작위로 배정했기 때문에 **"포화도 25% 클러스터의 미처치 개인"**과 **"포화도 75% 클러스터의 미처치 개인"**을 비교할 수 있습니다. 두 그룹 모두 개인적으로는 처치받지 않았지만 주변 환경의 처치 강도가 다릅니다.

이 차이가 순수한 간접 효과입니다. 위의 코드를 살펴보겠습니다.

1단계에서 30개 클러스터를 5가지 포화도 수준(0%, 25%, 50%, 75%, 100%)에 균등하게 배정합니다. 2단계에서 각 클러스터 내에서 해당 포화도만큼의 개인을 처치군으로 무작위 배정합니다.

결과 변수는 직접 효과 0.3과 포화도에 비례하는 간접 효과 0.4로 구성됩니다. 마지막으로 포화도별 평균 결과와 처치 효과를 비교합니다.

실제 현업에서는 어떻게 활용할까요? 소셜 게임에서 새로운 아이템을 도입한다고 가정해봅시다.

서버(클러스터)별로 아이템 배포 비율을 다르게 설정합니다. 10% 서버, 30% 서버, 50% 서버 등 다양하게 운영합니다.

이렇게 하면 "아이템을 가진 플레이어가 많은 환경"과 "적은 환경"에서 게임 경험이 어떻게 달라지는지 비교할 수 있습니다. 하지만 주의할 점도 있습니다.

이중 무작위 설계는 구현이 복잡합니다. 각 클러스터의 포화도를 정확히 맞추기 위해 정교한 시스템이 필요합니다.

또한 포화도 수준이 많아질수록 필요한 클러스터 수도 늘어납니다. 실험 규모가 커지면 비용과 운영 복잡성도 증가합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 분석 결과를 본 김개발 씨가 감탄했습니다.

"포화도가 올라갈수록 미처치자의 결과도 좋아지네요. 이게 순수한 간접 효과군요!" 박시니어 씨가 고개를 끄덕였습니다.

"인과추론의 황금 표준이라고 할 수 있죠." 이중 무작위 설계를 이해하면 네트워크 효과를 가장 정확하게 추정할 수 있는 실험을 설계할 수 있습니다.

실전 팁

💡 - 포화도 수준은 3-5개 정도가 적당합니다 (너무 많으면 검정력이 분산됨)

  • 포화도 0%와 100%를 반드시 포함하여 양 극단을 측정하세요
  • 클러스터 크기가 다르면 가중치를 적용한 분석이 필요합니다

7. 시간에 따른 네트워크 효과 확산

김개발 씨의 분석 역량은 날로 발전했습니다. 어느 날 프로덕트팀에서 새로운 질문을 던졌습니다.

"효과가 시간이 지나면서 어떻게 퍼져나가는지 알고 싶어요. 1주 차와 4주 차의 간접 효과가 다를 것 같거든요." 이번에는 시간 차원을 추가해야 했습니다.

**시간적 확산(Temporal Diffusion)**은 네트워크 효과가 시간에 따라 어떻게 퍼져나가는지를 분석합니다. 초기에는 직접 연결된 1차 이웃에게만 영향이 가다가, 시간이 지나면 2차, 3차 이웃에게까지 확산됩니다.

마치 호수에 돌을 던졌을 때 파문이 점점 멀리 퍼져나가는 것과 같습니다.

다음 코드를 살펴봅시다.

import numpy as np
import pandas as pd
import networkx as nx

np.random.seed(42)
G = nx.barabasi_albert_graph(300, 3)
nodes = list(G.nodes())

# 초기 처치자 (시드 유저) - 10%
initial_treated = set(np.random.choice(nodes, size=30, replace=False))

# 시간에 따른 확산 시뮬레이션
def simulate_diffusion(graph, initial_seeds, time_steps, spread_prob=0.3):
    treated_over_time = {0: initial_seeds.copy()}
    currently_treated = initial_seeds.copy()

    for t in range(1, time_steps + 1):
        new_treated = set()
        for node in graph.nodes():
            if node in currently_treated:
                continue
            # 처치된 이웃이 있으면 확률적으로 전파
            treated_neighbors = sum(1 for n in graph.neighbors(node) if n in currently_treated)
            if treated_neighbors > 0:
                prob = 1 - (1 - spread_prob) ** treated_neighbors
                if np.random.random() < prob:
                    new_treated.add(node)

        currently_treated = currently_treated.union(new_treated)
        treated_over_time[t] = currently_treated.copy()

    return treated_over_time

diffusion_results = simulate_diffusion(G, initial_treated, time_steps=5)

print("시간별 처치자 수:")
for t, treated in diffusion_results.items():
    print(f"t={t}: {len(treated)}명 ({len(treated)/len(nodes)*100:.1f}%)")

# 확산 속도 분석
for t in range(1, 6):
    new_adopters = len(diffusion_results[t]) - len(diffusion_results[t-1])
    print(f"t={t}에 새로 추가: {new_adopters}명")

김개발 씨는 2주간의 A/B 테스트 결과를 보고하려던 참이었습니다. 그런데 프로덕트 매니저가 질문했습니다.

"2주로 충분한가요? 입소문 효과는 시간이 지나야 나타나지 않나요?" 박시니어 씨가 끼어들었습니다.

"좋은 지적이에요. 네트워크 효과에는 시간 차원이 있거든요." 그렇다면 시간적 확산이란 무엇일까요?

쉽게 비유하자면, 이것은 마치 유행가가 퍼지는 과정과 같습니다. 처음에는 음악 매니아들만 새 노래를 듣습니다.

그들이 친구에게 추천하면, 그 친구들도 듣기 시작합니다. 시간이 지나면 친구의 친구, 그 친구의 친구까지 퍼져나갑니다.

처음에는 천천히, 중간에는 빠르게, 그리고 포화 상태가 되면 다시 느려집니다. 왜 시간 차원을 고려해야 할까요?

짧은 기간의 실험 결과만 보면 간접 효과를 과소 추정할 수 있습니다. 1주 차에는 직접 연결된 친구에게만 영향이 갑니다.

하지만 4주 차에는 친구의 친구, 8주 차에는 더 먼 네트워크까지 효과가 퍼집니다. 실험 기간이 짧으면 이 장기 효과를 놓치게 됩니다.

바로 이런 현상을 분석하는 것이 시간적 확산 모델입니다. **확산 모델(Diffusion Model)**에서는 각 시점에서 미채택자가 채택자로 전환될 확률을 모델링합니다.

가장 간단한 형태는 **독립 캐스케이드 모델(Independent Cascade Model)**입니다. 채택자인 이웃 하나당 일정 확률로 영향을 받고, 여러 이웃의 영향이 독립적으로 작용합니다.

위의 코드를 살펴보겠습니다. 먼저 300개 노드의 스케일-프리 네트워크를 생성합니다.

10%인 30명을 초기 시드로 설정합니다. simulate_diffusion 함수에서 각 시점마다 미처치 노드가 처치된 이웃 수에 따라 확률적으로 전환됩니다.

전환 확률은 1 - (1 - 0.3)^k 공식을 따릅니다. 여기서 k는 처치된 이웃 수입니다.

결과를 보면 초기에는 확산이 빠르다가 점점 느려지는 S자 곡선을 확인할 수 있습니다. 실제 현업에서는 어떻게 활용할까요?

바이럴 마케팅 캠페인을 계획한다고 가정해봅시다. 초기 시드 유저를 신중하게 선정해야 합니다.

네트워크 중심에 있는 인플루언서에게 먼저 제품을 제공하면 확산 속도가 빨라집니다. 시간에 따른 확산 패턴을 시뮬레이션하면 캠페인 기간과 예상 도달률을 계획할 수 있습니다.

하지만 주의할 점도 있습니다. 실제 확산은 모델보다 복잡합니다.

시간이 지나면 기억이 사라지거나 관심이 줄어드는 **감쇠 효과(decay effect)**가 있습니다. 또한 경쟁 제품이나 부정적 입소문도 함께 확산될 수 있습니다.

단순 모델의 예측을 맹신하지 말고 실제 데이터로 검증해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

시뮬레이션 결과를 본 김개발 씨가 말했습니다. "4주 차가 되면 거의 60%까지 확산되네요.

2주 실험은 너무 짧았어요." 프로덕트 매니저가 고개를 끄덕였습니다. "다음 실험은 더 길게 해봅시다." 시간적 확산을 이해하면 실험 기간을 적절히 설정하고 장기 효과를 예측할 수 있습니다.

실전 팁

💡 - 실험 기간은 최소 2-3차 이웃까지 확산되는 시간을 고려하세요

  • 주간/월간 단위로 효과 변화를 추적하여 확산 패턴을 파악하세요
  • 초기 시드 선정이 장기 확산에 미치는 영향을 분석하세요

8. 인과 그래프를 활용한 간섭 모델링

김개발 씨는 이제 네트워크 효과의 다양한 측면을 이해하게 되었습니다. 하지만 복잡한 상황에서 무엇이 무엇에 영향을 주는지 정리가 안 될 때가 있었습니다.

박시니어 씨가 새로운 도구를 소개했습니다. "인과 그래프로 정리하면 한눈에 보여요."

인과 그래프(Causal Graph) 또는 **DAG(Directed Acyclic Graph)**는 변수들 사이의 인과 관계를 화살표로 표현한 그림입니다. 네트워크 효과 상황에서 내 처치, 친구의 처치, 내 결과, 친구의 결과 사이의 관계를 명확히 보여줍니다.

마치 가계도가 가족 관계를 한눈에 보여주듯, 인과 그래프는 변수 관계를 보여줍니다.

다음 코드를 살펴봅시다.

# 인과 그래프 표현을 위한 간단한 구조
# 화살표: A -> B 는 "A가 B에 영향을 줌"을 의미

causal_structure = {
    'nodes': [
        'Z_i',      # 개인 i의 처치
        'Z_j',      # 이웃 j의 처치
        'Y_i',      # 개인 i의 결과
        'Y_j',      # 이웃 j의 결과
        'X_i',      # 개인 i의 사전 특성
        'G_ij'      # i와 j의 네트워크 연결
    ],
    'edges': [
        ('Z_i', 'Y_i'),    # 직접 효과
        ('Z_j', 'Y_i'),    # 간접 효과 (간섭)
        ('Z_j', 'Y_j'),    # 이웃의 직접 효과
        ('Y_j', 'Y_i'),    # 이웃 결과가 내 결과에 영향 (사회적 영향)
        ('X_i', 'Z_i'),    # 특성이 처치에 영향 (선택 편향)
        ('X_i', 'Y_i'),    # 특성이 결과에 영향
        ('G_ij', 'Z_j'),   # 네트워크가 이웃 처치에 영향
        ('G_ij', 'Y_i')    # 네트워크가 결과에 영향
    ]
}

# 인과 효과 식별을 위한 조건 분석
def identify_confounders(graph, treatment, outcome):
    """처치와 결과 사이의 교란 변수 식별"""
    confounders = []
    for node in graph['nodes']:
        if node == treatment or node == outcome:
            continue
        # 단순화: 처치와 결과 모두에 영향을 주는 변수
        affects_treatment = (node, treatment) in graph['edges']
        affects_outcome = (node, outcome) in graph['edges']
        if affects_treatment and affects_outcome:
            confounders.append(node)
    return confounders

# 직접 효과 추정 시 통제해야 할 변수
confounders_direct = identify_confounders(causal_structure, 'Z_i', 'Y_i')
print(f"직접 효과(Z_i -> Y_i) 추정 시 통제 변수: {confounders_direct}")

# 간접 효과 추정 시 통제해야 할 변수
confounders_indirect = identify_confounders(causal_structure, 'Z_j', 'Y_i')
print(f"간접 효과(Z_j -> Y_i) 추정 시 통제 변수: {confounders_indirect}")

김개발 씨는 분석 결과를 정리하다가 머리가 복잡해졌습니다. 내 처치가 내 결과에 영향, 친구 처치가 내 결과에 영향, 그런데 네트워크 구조도 영향을 주고...

도대체 무엇을 통제해야 하는 걸까요? 박시니어 씨가 빈 종이를 꺼내며 말했습니다.

"이럴 때는 인과 그래프를 그려보면 정리가 돼요." 그렇다면 인과 그래프란 무엇일까요? 쉽게 비유하자면, 이것은 마치 도시의 지하철 노선도와 같습니다.

복잡한 도시 구조를 단순한 선과 점으로 표현하면 어느 역에서 환승해야 하는지 한눈에 보입니다. 인과 그래프도 마찬가지로 복잡한 변수 관계를 화살표로 단순화하여 어떤 변수를 통제해야 하는지 파악할 수 있게 해줍니다.

왜 인과 그래프가 필요할까요? 네트워크 효과 상황에서는 많은 변수가 서로 연결되어 있습니다.

내 처치 Z_i, 친구 처치 Z_j, 내 결과 Y_i, 친구 결과 Y_j, 네트워크 구조 G_ij, 사전 특성 X_i 등이 복잡하게 얽혀 있습니다. 그냥 회귀분석을 돌리면 **선택 편향(selection bias)**이나 **충돌 편향(collider bias)**에 빠질 수 있습니다.

바로 이런 문제를 해결하는 것이 인과 그래프 분석입니다. **DAG(Directed Acyclic Graph)**에서는 화살표가 인과 방향을 나타냅니다.

A -> B는 "A가 B의 원인"임을 의미합니다. **교란 변수(confounder)**는 처치와 결과 모두에 영향을 주는 변수입니다.

이 변수를 통제하지 않으면 인과효과를 잘못 추정합니다. 반면 **매개 변수(mediator)**는 통제하면 안 됩니다.

효과의 일부를 차단해버리기 때문입니다. 위의 코드를 살펴보겠습니다.

인과 그래프를 딕셔너리로 표현했습니다. 노드는 6개 변수, 간선은 8개 인과 관계입니다.

identify_confounders 함수는 처치와 결과 모두에 영향을 주는 변수를 찾습니다. 직접 효과 Z_i -> Y_i를 추정할 때는 X_i를 통제해야 합니다.

간접 효과 Z_j -> Y_i를 추정할 때는 G_ij를 통제해야 합니다. 실제 현업에서는 어떻게 활용할까요?

친구 추천 기능의 효과를 분석한다고 가정해봅시다. 인과 그래프를 그려보면 숨겨진 교란 변수가 보일 수 있습니다.

예를 들어 "활동성 높은 사용자"는 추천도 많이 받고 앱 사용도 많이 합니다. 이 변수를 통제하지 않으면 추천의 효과를 과대 추정하게 됩니다.

하지만 주의할 점도 있습니다. 인과 그래프는 분석가의 도메인 지식에 기반합니다.

잘못된 그래프를 그리면 잘못된 결론에 도달합니다. 모든 관계를 데이터로 검증할 수는 없습니다.

따라서 도메인 전문가, 프로덕트 팀과 함께 그래프를 검토하고 합의해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

그래프를 그린 김개발 씨가 말했습니다. "아, 네트워크 구조가 교란 변수였군요.

이걸 통제 안 하면 간접 효과가 왜곡되겠네요." 박시니어 씨가 미소 지었습니다. "그래프를 그리니까 명확해지죠?" 인과 그래프를 활용하면 복잡한 네트워크 상황에서도 올바른 분석 전략을 수립할 수 있습니다.

실전 팁

💡 - 분석 전에 항상 인과 그래프를 먼저 그려보세요

  • 팀원들과 그래프를 공유하고 빠진 변수가 없는지 검토하세요
  • do-calculus나 backdoor criterion을 공부하면 더 정교한 분석이 가능합니다

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Python#NetworkEffects#CausalInference#ABTesting#Interference#SUTVA#Data Science

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.