멤버십 리워드 프로그램의 효과 추정하기

Estimating the effect of a Member Rewards program

DoWhy 를 사용하여 고객에 대한 구독 또는 리워드 프로그램의 효과를 추정하는 방법을 알아보겠습니다.

고객이 웹사이트에 가입하면 추가 혜택을 받는 멤버십 리워드 프로그램이 있다고 가정해봅시다. 해당 프로그램이 효과적인지 어떻게 알 수 있을까요? 여기서 우리는 인과적인 질문을 할 수 있습니다.

멤버십 리워드 프로그램 제공이 총 매출에 미치는 영향은 무엇인가?

그리고 이와 동등한 반사실적(counterfactual) 질문은 다음과 같습니다.

만약 고객들이 멤버십 리워드 프로그램에 가입하지 않았다면, 그들은 웹사이트에 얼마나 더 적은 돈을 썼을 것인가?

다시 말하면, 우리는 처치집단에 대한 평균 처치 효과(the Average Treatment Effect On the Treated, ATT)를 구하고자 합니다.

I. Formulating the causal model

리워드 프로그램이 2019년 1월에 도입되었다고 가정해봅시다. 결과 변수는 연말 총 지출액입니다. 우리는 모든 유저의 모든 월별 거래 내역과 리워드 프로그램 가입을 선택한 유저들의 가입 시간에 대한 데이터를 가지고 있습니다. 데이터는 다음과 같습니다.

# Creating some simulated data for our example example
import pandas as pd
import numpy as np
num_users = 10000
num_months = 12

signup_months = np.random.choice(np.arange(1, num_months), num_users) * np.random.randint(0,2, size=num_users)
df = pd.DataFrame({
    'user_id': np.repeat(np.arange(num_users), num_months),
    'signup_month': np.repeat(signup_months, num_months), # signup month == 0 means customer did not sign up
    'month': np.tile(np.arange(1, num_months+1), num_users), # months are from 1 to 12
    'spend': np.random.poisson(500, num_users*num_months) #np.random.beta(a=2, b=5, size=num_users * num_months)*1000 # centered at 500
})
# Assigning a treatment value based on the signup month
df["treatment"] = (1-(df["signup_month"]==0)).astype(bool)
# Simulating effect of month (monotonically increasing--customers buy the most in December)
df["spend"] = df["spend"] - df["month"]*10
# The treatment effect (simulating a simple treatment effect of 100)
after_signup = (df["signup_month"] < df["month"]) & (df["signup_month"] !=0)
df.loc[after_signup,"spend"] = df[after_signup]["spend"] + 100
df
user_idsignup_monthmonthspendtreatment

0

0

6

1

526

True

1

0

6

2

464

True

2

0

6

3

473

True

3

0

6

4

502

True

4

0

6

5

436

True

...

...

...

...

...

...

119995

9999

7

8

533

True

119996

9999

7

9

518

True

119997

9999

7

10

485

True

119998

9999

7

11

504

True

119999

9999

7

12

459

True

120000 rows × 5 columns

The importance of time

이 문제를 모델링하는 데 있어서 시간이 중요한 역할을 합니다.

리워드 프로그램 가입은 향후 거래에 영향을 미칠 수 있지만, 이전 거래에는 영향을 미치지 않습니다. 사실 리워드 가입 이전의 거래는 리워드 가입 결정을 유발한다고 가정할 수 있습니다.

따라서 각 유저의 변수들을 다음과 같이 나눌 수 있습니다.

  1. 처치 전 활동 (처치의 원인)

  2. 처치 후 활동 (처치 적용 결과)

물론 가입과 총 지출에 영향을 미치는 많은 중요한 변수가 누락되어 있습니다(e.g., 구입한 제품 유형, 유저 계정 사용 기간, 지역 등). 관측되지 않은 교란 변수(Unobserved Confounders)를 나타내는 노드가 필요합니다.

아래는 i=3 개월에 가입한 유저에 대한 인과 그래프입니다. 모든 i에 대해서 분석은 유사할 것입니다.

import os, sys
sys.path.append(os.path.abspath("../../../"))
import dowhy

# Setting the signup month (for ease of analysis)
i = 3
causal_graph = """digraph {
treatment[label="Program Signup in month i"];
pre_spends;
post_spends;
Z->treatment;
U[label="Unobserved Confounders"];
pre_spends -> treatment;
treatment->post_spends;
signup_month->post_spends; signup_month->pre_spends;
signup_month->treatment;
U->treatment; U->pre_spends; U->post_spends;
}"""

# Post-process the data based on the graph and the month of the treatment (signup)
df_i_signupmonth = df[df.signup_month.isin([0,i])].groupby(["user_id", "signup_month", "treatment"]).apply(
    lambda x: pd.Series({'pre_spends': np.sum(np.where(x.month < i, x.spend,0))/np.sum(np.where(x.month<i, 1,0)),
                        'post_spends': np.sum(np.where(x.month > i, x.spend,0))/np.sum(np.where(x.month>i, 1,0)) })
).reset_index()
print(df_i_signupmonth)
model = dowhy.CausalModel(data=df_i_signupmonth,
                     graph=causal_graph.replace("\n", " "),
                     treatment="treatment",
                     outcome="post_spends")
model.view_model()
from IPython.display import Image, display
display(Image(filename="causal_model.png"))
 user_id  signup_month  treatment  pre_spends  post_spends
0           0             6       True       480.2   511.000000
1           2             0      False       477.8   408.166667
2           5             0      False       472.6   423.333333
3           6             6       True       465.2   505.833333
4           8             0      False       447.6   396.333333
...       ...           ...        ...         ...          ...
5446     9993             0      False       488.8   415.166667
5447     9994             0      False       471.4   392.666667
5448     9995             0      False       489.4   402.666667
5449     9997             0      False       498.0   414.333333
5450     9998             0      False       469.6   400.000000

[5451 rows x 5 columns]

더 일반적으로, 고객에 대한 모든 활동 데이터를 위 그래프에 포함시킬 수 있습니다. 모든 사전 및 사후 활동 데이터는 이미 사용된 사전 및 사후 노드들과 동일한 위치 및 엣지를 차지합니다.

II. Identifying the causal effect

만약 관측되지 않은 교란 변수가 큰 역할을 하지 않는다고 가정해봅시다.

identified_estimand = model.identify_effect(proceed_when_unidentifiable=True)
print(identified_estimand)
Estimand type: nonparametric-ate

### Estimand : 1
Estimand name: backdoor
Estimand expression:
     d
────────────(Expectation(post_spends|signup_month,pre_spends))
d[treatment]
Estimand assumption 1, Unconfoundedness: If U→{treatment} and U→post_spends then P(post_spends|treatment,signup_month,pre_spends,U) = P(post_spends|treatment,signup_month,pre_spends)

### Estimand : 2
Estimand name: iv
Estimand expression:
Expectation(Derivative(post_spends, [Z])*Derivative([treatment], [Z])**(-1))
Estimand assumption 1, As-if-random: If U→→post_spends then ¬(U →→{Z})
Estimand assumption 2, Exclusion: If we remove {Z}→{treatment}, then ¬({Z}→post_spends)

### Estimand : 3
Estimand name: frontdoor
No such variable found!

DoWhy 는 그래프를 바탕으로, 가입 월과 처지 이전 월(signup_month, pre_spend)에 소요된 금액을 조건화할 필요가 있다고 판단합니다.

III. Estimating the effect

이제 backdoor estimand(추정치)를 기반으로 target_units 를 “att”로 설정하여 효과를 추정합니다.

estimate = model.estimate_effect(identified_estimand,
                                 method_name="backdoor1.propensity_score_matching",
                                target_units="att")
print(estimate)
*** Causal Estimate ***

## Identified estimand
Estimand type: nonparametric-ate

## Realized estimand
b: post_spends~treatment+signup_month+pre_spends
Target units: att

## Estimate
Mean value: 115.21872571872572

위와 같이 평균 처치 효과를 알려줍니다. 즉, i=3 개월에 리워드 프로그램에 등록한 고객의 총 지출에 대한 평균 효과입니다. i 값을 변경한 후 분석을 다시 실행하여 다른 달에 가입한 고객에 대한 효과를 비슷한 방법으로 계산할 수 있습니다.

다만 좌측 및 우측 관측 중단으로 인해 효과 추정에 어려움을 겪는 경우가 존재합니다.

  1. 좌측 관측 중단 (Left-censoring)

    : 고객이 첫 달에 가입하는 경우, 우리는 가입하지 않은 고객과 비교할 만큼 충분한 거래 이력이 없습니다. 따라서 backdoor identified estimand 를 적용해야 합니다.

  2. 우측 관측 중단 (Right-censoring)

    : 고객이 마지막 달에 가입하는 경우, 가입 후 결과를 추정하기에는 향후(사후) 거래 이력이 부족합니다.

따라서 아무리 가입 효과가 모든 달에 걸쳐 동일했더라도, 데이터가 부족한 상황에서는 추정된 pre-treatment 또는 post-treatment 거래 활동의 변동이 클 수 있기 때문에 가입 월별로 추정 효과가 다를 수 있습니다.

IV. Refuting the estimate

Placebo treatment refuter 를 사용하여 추정치를 반박합니다. 이 refuter 는 treatment 를 독립적인 랜덤 변수로 대체하고 추정치가 0이 되는지 여부를 확인합니다. (0이 되어야 합니다!)

refutation = model.refute_estimate(identified_estimand, estimate, method_name="placebo_treatment_refuter",
                     placebo_type="permute", num_simulations=2)
print(refutation)
Refute: Use a Placebo Treatment
Estimated effect:115.21872571872572
New effect:1.3245920745920756
p value:0.015071603412401298

Last updated