데이터로그😎

[데이터리안_플젝2] A/B test 본문

SQL/프로젝트

[데이터리안_플젝2] A/B test

지연v'_'v 2024. 1. 8. 18:20
🚨 *데이터리안의 무료 강의를 참고했습니다. Mode에서 제공하는 소스코드는 제가 사용하는 MySQL에는 맞지않아, MySQL에 맞도록 코드를 직접 짰습니다. 따라서 문제점이 발견될 수 있습니다. 수정 사항을 발견하시면 언제든 댓글 주세요. 분석에 사용한 자료들의 출처와 상세 코드는 맨아래출처 부분에서 확인할 수 있습니다.

 

 

A/B test란 무엇일까?

  • 정의
    • 웹 사이트 방문자를 임의로 두 집단으로 나누고, 한 집단에는 기존 사이트를 보여주고 다른 집단에게는 새로운 사이트를 보여준 다음, 두 집단 중 어떤 집단이 더 높은 *성과를 보이는지를 측정하여 새 사이트가 기존 사이트에 비해 좋은지를 정량적으로 평가하는 방식을 의미한다.
    • *성과: 회원 가입율, 재방문율, 구매전환율 등이 될 수 있다. (목표에 따라 다르게 설정된다.)
  • 목표
    • 상관관계로부터 인과관계를 찾아내기 위함이다. 즉, 원인과 결과를 찾아내기 위함인데, 이를 찾아내야만 원인에 해당하는 요소에 개입하여 결과에 해당하는 요소가 우리가 원하는 방향으로 변화할 수 있기 때문이다. 
    • 혹은 이미 결과에 변화가 생겼을 때, 이 변화의 원인이 우리가 했던 개입 때문이 맞는지를 판단할 수 있기 때문이다.
  • 예시
    • 어떤 쇼핑몰 웹 사이트가 있다. 3개월에 걸쳐 디자인 개편 프로젝트를 진행했고 지난주에 성공적으로 새 디자인을 적용했다. 그랬더니 새 디자인을 적용하기 전에 비해 일 매출이 10% 증가했다. 매출 증가가 새 디자인 도입 덕분이라고 볼 수 있을까?  
    • 새 디자인 적용과 같은 내부 요인이 직접적인 원인일 수 있다. 그러나 외부 요인이 작용했을 수도 있다. 하필 새로운 디자인이 적용된 날에 경쟁 쇼핑몰이 문을 닫았다거나, 하필 그 날 쇼핑몰에 새로운 경쟁력있는 상품이 있고 되었다거나 하는 요인과 같은 외부 요인이 있을 수 있다. 만약 새로운 경쟁력있는 상품이 입고 되어 매출이 10% 증가했다면, 매출 증가는 디자인 팀 덕분이 아니라 영업팀 덕이 된다. 
    • A/B 테스팅을 하게 되면 방문자를 임의로 A,B 두 집단으로 나누고 A 집단에게는 기존 디자인을, B집단에게는 새로운 디자인을 보여주며 테스트를 진행한다. 새로운 디자인을 적용하기 전후의 매출을 비교하는 것이 아니라, 동시에 A, B 두집단에게 테스팅이 진행하여 두 집단의 매출을 비교하면 시간의 흐름에 따라 발생하는 변화를 (경쟁 쇼핑몰 부도, 신상품 입고 등)을 통제할 수 있기 때문에 순수한 디자인 변화로 인한 매출 차이를 가려낼 수 있다. 만약 A 집단에 비해 B 집단에서 매출 증가가 있었다면 "디자인 개편과 매출 증가 사이에는 인과관계가 성립할 가능성이 크다" 라고 말할 수 있다.
  • 출처: https://boxnwhis.kr/2015/01/29/a_b_testing.html

 

 

개요

  • 상황
    • Yammer의 publisher 기능 개선을 위해 A/B test를 진행했다.
    • 테스트 기간: 2014.06월 한 달간
    • A/B 그룹
      1. A 그룹: shown the old version of publisher (control group, 대조군)
      2. B 그룹: shown the new version of publisher (test group, 실험군)
  • 데이터 정보 (테이블)
    테이블 명 설명 컬럼 정보
    users user에 관한
    모든 정보
    - user_id : 유저 고유의 아이디
    - created_at : 유저 계정 생성일
    - company_id : 유저가 속한 회사 ID
    - language : 유저 사용 언어
    - activated_at : 유저 계정의 활성일, state가 active로 변경된 일자
    - state : 계정 상태 (active / pending)
    events user가 발생시킨
    모든 event에 관한
    정보 (log)
    - user_id: 유저 고유의 아이디
    - occurred_at : event 발생일자
    - event_type : engagement / signup_flow (가입)
    - event_name: login / home_page / like_message / send_message 등
    - location : 사용 위치
    - device : 접속 기기
    - user_type : 유저 타입
    experiments 실험 관련 정보
    (A/B test 뿐만 아니라 기타 실험들에 대한 정보 포함)
    - user_id : 유저 고유의 아이디
    - occurred_at : 실험 일자
    - experiment : 실험 종류
    - experiment_group : control group / test group
    - location : 위치
    - device : 기기
    - user_type : 유저 타입

 

[분석1] 평균 메시지 발송건수 비교

A/B test 성과지표

  • A/B test 결과의 성과지표'유저 당 평균 메시지 발송건수' 로 지정.
  • '유저 당 평균 메시지 발송건수' 를 구하기 위해 도출해야하는 데이터
    • ① 각 그룹에 포함된 유저 수
    • ② 각 그룹의 메시지 발송건수
    • ③  각 그룹의 유저 당 평균 메시지 발송건수 (③=②/① 의 식으로 구할 수 있다)

쿼리

  1. JOIN 절을 통해 experiments, users, events 테이블을 모두 합쳤다.
  2. ON 절에서 아래의 데이터만 추출했다.
    • 실험 종류는 publilsher update이다. 
    • event는 실험 기간 동안에 발생한 것이어야 한다. (2014-06월 동안)
    • event_type은 engagement여야 한다. (event_type은 sign up과 관련된 것이 아니어야 한다)
  3. experiment_group( control_group, test_group) 에 따라 GROUP BY 진행하여 집계를 한다.
    • ① 각 그룹에 포함된 유저 수 (user_cnt) = COUNT(DISTINCT ex.user_id)
    • ② 각 그룹의 메시지 발송건수 (metric) = event_name이 'send_message'인 user_id 수를 count한다.
    • ③  각 그룹의 유저 당 평균 메시지 발송건수 (average)  =  ②/①
      query = " SELECT  experiment_group, COUNT(DISTINCT ex.user_id) user_cnt, \
                          COUNT(CASE WHEN event_name = 'send_message' THEN ex.user_id ELSE NULL END) as metric,\
                          COUNT(CASE WHEN event_name = 'send_message' THEN ex.user_id ELSE NULL END) / COUNT(DISTINCT ex.user_id) as average \
                  FROM experiments ex \
                      JOIN users u ON ex.user_id = u.user_id \
                      JOIN events e on ex.user_id = e.user_id \
                      AND ex.experiment = 'publisher_update' \
                      AND e.occurred_at LIKE '%2014-06%' \
                      AND e.event_type = 'engagement'  \
                  GROUP BY experiment_group"
      
      message_cnt_df = pd.read_sql(query, connection)
      message_cnt_df

최종 데이터 및 그래프

  • 최종 데이터
    message_cnt_df
  • 그래프
    import seaborn as sns
    import matplotlib.pyplot as plt
    
    plt.figure(figsize=(10,6))
    sns.set_theme(palette='deep')
    ax = sns.barplot(data=message_cnt_df, x='experiment_group', y='average')
    plt.xlabel('Experiment Group',fontsize=14)
    plt.ylabel('Average Message Sent', fontsize=14)
    plt.xticks(fontsize=14)
    plt.yticks(fontsize=14)
    
    for p in ax.patches:
        ax.annotate(p.get_height(), (p.get_x()+p.get_width()/2, p.get_height()),
                    fontsize=12,color='red', ha='center',va='center', textcoords='offset points')
    
    
    plt.show()

각 그룹의 유저당 평균 메시지 발송건수

분석1 결론

  • test group은 테스트 기간 동안 평균 4회의 메시지를 발송했고, control group은 평균 2.6회의 메시지를 발송했다.
  • test group의 평균 메시지 발송 수가 control group 보다 50% 이상 많음
  • test group과 control group 사이에 지표 차이가 나는 것은 좋지만 왜 이렇게까지 많이 차이가 날까? 1.5배 차이라니
  • 아래의 원인들 때문에 평균 메시지 발송건수 지표가 이렇게나 많이 차이나는 것은 아닐까?
    1.  잘못된 성과 지표 선정: 분석1에서 사용했던 '유저당 평균 메시지 발송건수' 라는 지표가 publisher 개선에 따른 결과를 잘 반영하지 못할 수 있다.즉, 메시지 발송건수가 publisher 개선 프로젝트의 성공을 측정하는 지표가 아닐 수도 있다. 다른 지표도 함께 살펴보아야 한다.
    2. 샘플링의 오류: Control, Test group이 제대로 나뉘지 않았을 수도 있다. Randomly하게 실험군, 대조군을 나누어야 하는데 랜덤하지 못했을 수 있다. 예를 들면 [실험군:여자, 대조군:남자]로 나뉘거나 [실험군: 10,20대, 대조군:30대 이상] 으로 나뉘거나 했을 수 있다. 이때 X(원인)는 publisher version이고 Y(결과)는  Average Message Sent 수 여야하는데, Y에 영향을 미치는 것이 X이외에 다른 것이 있다면 실험이 잘못된 것이다. 
  • 앞으로의 분석은 아래와 같이 진행할 것이다.
    • 새로운 성과 지표 선정
    • 그룹의 샘플링이 제대로 되어있는지 확인

 

[분석2-1] 로그인 횟수 분석

새로운 A/B test 성과 지표

  • '유저당 평균 메시지 발송건수' 는 Test group > Control group 의 결과가 나왔다. 목표하던 바대로 결과가 나왔지만 문제는 Test, Control group 간에 너무 큰 차이를 보인다는 것이다. 그래서 이 결과가 과연 믿을 수 있을만한 것인가? 라는 의심이 간다..!
  • 그래서 '유저의 평균 로그인 횟수''유저의 평균 로그인 일수'  라는 새로운 지표를 세우고 다시 분석을 하려 한다.
    •  '유저의 평균 로그인 횟수'  를 구하기 위해 도출해야하는 데이터
      • ① 각 그룹에 포함된 유저 수
      • ② 각 그룹의 메시지 로그인 횟수
      • ③  각 그룹의 유저 당 평균 로그인 횟수 (③=②/① 의 식으로 구할 수 있다)
    •  '유저의 평균 로그인 일수'  를 구하기 위해 도출해야하는 데이터
      • ① 각 그룹에 포함된 유저 수
      • ② 각 그룹의 메시지 로그인 일수
      • ③  각 그룹의 유저 당 평균 로그인 일수 (③=②/① 의 식으로 구할 수 있다)

쿼리

  1. JOIN 절을 통해 experiments, users, events 테이블을 모두 합쳤다.
  2. ON 절에서 아래의 데이터만 추출했다.
    • 실험 종류는 publilsher update이다. 
    • event는 실험 기간 동안에 발생한 것이어야 한다. (2014-06월 동안)
    • event_type은 engagement여야 한다. (event_type은 sign up과 관련된 것이 아니어야 한다)
  3. experiment_group( control_group, test_group) 에 따라 GROUP BY 진행하여 집계를 한다.
    • ① 각 그룹에 포함된 유저 수 (user_cnt) = COUNT(DISTINCT ex.user_id)
    • ② 각 그룹의 로그인 횟수(login_cnt) = event_name이 'login'인 user_id 수를 count한다.
    • ③  각 그룹의 유저 당 평균 로그인 횟수 (average)  =  ②/①
      query = " SELECT  experiment_group, COUNT(DISTINCT ex.user_id) user_cnt, COUNT(CASE WHEN event_name ='login' THEN ex.user_id ELSE NULL END) as login_cnt, \
                      	COUNT(CASE WHEN event_name ='login' THEN ex.user_id ELSE NULL END)/ COUNT(DISTINCT ex.user_id) average \
                  FROM experiments ex \
                      JOIN users u ON ex.user_id = u.user_id \
                      JOIN events e on ex.user_id = e.user_id \
                      AND ex.experiment = 'publisher_update' \
                      AND e.occurred_at LIKE '%2014-06%' \
                      AND e.event_type = 'engagement'   \
                  GROUP BY experiment_group"
      
      login_cnt_df = pd.read_sql(query, connection)
      login_cnt_df

최종 데이터 및 그래프

  • 최종 데이터
    login_cnt_df
  • 그래프
ax = sns.barplot(data= login_cnt_df, x='experiment_group', y='average')
for p in ax.patches:
    ax.annotate(p.get_height(), (p.get_x()+p.get_width()/2, p.get_height()),
                fontsize=12,color='red', ha='center',va='center', textcoords='offset points')

각 그룹의 평균 로그인 횟수

 

분석2-1 결론

  • 평균 로그인 수는 대조군이 평균 3.3번, 실험군이 평균 4.1번으로, 실험기간 중 평균 로그인 수도 실험군이 더 높다.
  • 즉, test group의 유저들은 한 달 동안 평균 4.1회, control group의 유저들은 한 달 동안 평균 3.3회 로그인 했다. 
  • 그러나, 로그인 횟수가 4회라는 것은 4일동안 하루에 1회 로그인한 것일 수도 있고, 하루 동안 4회 로그인한 것일 수도 있다. 만약 후자라면 하루에 로그인-로그아웃을 계속 반복하고 있다는 소리이기 때문에 좋은 시그널일 수 없다.
  • 따라서 로그인 일수도 확인해봐야 한다.

 

[분석2-2] 로그인 일수 분석

쿼리

  1. 서브쿼리
    • JOIN 절을 통해 experiments, users, events 테이블을 모두 합쳤다.
    • ON 절에서 아래의 데이터만 추출했다.
      • 실험 종류는 publilsher update이다. 
      • event는 실험 기간 동안에 발생한 것이어야 한다. (2014-06월 동안)
      • event_type은 engagement여야 한다. (event_type은 sign up과 관련된 것이 아니어야 한다)
    • experiment_group( control_group, test_group) 에 따라 GROUP BY 진행하여 집계를 한다.
      • 실험 그룹, user_id 별로 활동일 수를 계산한다.
  2. 외부쿼리
    • 다시 한 번 실험그룹에 따라 집계를 한다. 이 때 day의 평균값을 구한다.
query = " SELECT experiment_group, avg(day) as login_day_avg \
            FROM \
                (SELECT experiment_group, e.user_id, COUNT(DISTINCT DATE_FORMAT(e.occurred_at, '%Y-%m-%d')) as day \
                    FROM experiments ex \
                        JOIN users u ON ex.user_id = u.user_id \
                        JOIN events e on ex.user_id = e.user_id \
                        AND ex.experiment = 'publisher_update' \
                        AND e.occurred_at LIKE '%2014-06%' \
                        AND e.event_type = 'engagement'   \
                    GROUP BY experiment_group, e.user_id) a \
            GROUP BY 1"

login_day_df = pd.read_sql(query, connection)
login_day_df

 

최종 데이터 및 그래프

  • 최종 데이터

 

  • 그래프
    ax2= sns.barplot(data = login_day_df, x='experiment_group', y='login_day_avg')
    for p in ax2.patches:
        ax2.annotate(p.get_height(), (p.get_x()+p.get_width()/2, p.get_height()),
                    fontsize=12,color='red', ha='center',va='center', textcoords='offset points')​

 

분석2 결론

  • test group의 유저들은 한 달 동안 평균 3일, control group의 유저들은 한 달 동안 평균 3.6일 로그인 했다.
  • 실험 기간 동안에 control group과 test group의 평균 로그인 횟수, 평균 로그인 일수를 계산해봤더니 모두 control < test group
  • 실험 기간동안 실험군이 대조군 대비 유저 당 평균 0.8회 더 많이 로그인 했다.
  • 실험 기간동안 실험군이 대조군 대비 유저 당 평균 0.6일 더 많이 로그인 했다.

 

[분석3] 샘플링 공정성 확인

Test, Control group을 나눌 때 과연 샘플링이 공정하게 되었는가?

  • 쿼리
query = " SELECT DATE_FORMAT(u.activated_at, '%Y-%m') as month_activated,   \
                COUNT(CASE WHEN e.experiment_group = 'control_group' THEN u.user_id ELSE NULL END) as control_users, \
                COUNT(CASE WHEN e.experiment_group = 'test_group' THEN u.user_id ELSE NULL END) as test_users \
            FROM experiments e \
            JOIN users u \
                ON e.user_id = u.user_id \
            GROUP BY 1 \
            ORDER BY 1"

sampling_df = pd.read_sql(query, connection)
sampling_df

 

  • 최종 데이터

 

  • 그래프
plt.figure(figsize=(10,6))
sns.lineplot(data=sampling_df, x='month_activated', y='control_users')
sns.lineplot(data=sampling_df, x='month_activated', y='test_users')
plt.xticks(rotation=45)
plt.show()

 

분석3 결과

  • 2014.06월에 가입한 user들은 모두 control group에 포함되어 있다.
  • A/B test를 실행한 6월에 가입한 user들은 이전에 가입한 user들에 비해 로그인, 메시지 발송 등 활동할 기회가 적다. (활동기간이 촉박하기 때문). 따라서 6월 가입자들의 로그인 지표, 활동 지표는 낮은게 당연
  • 그런데 이러한 6월 가입자들이 모두 control group에 포함되어 있다. 이러한 이유로 control user group의 성과 지표가 좋지 않게 나온 것일 수도 있다.
  • 6월 가입자들을 모두 제외하고 다시 메시지 발송건수를 분석해보자!

 

[분석4] 6월 가입자 제외 후 분석

  • 쿼리
    query = " SELECT  experiment_group, COUNT(DISTINCT ex.user_id) user_cnt, \
                        COUNT(CASE WHEN event_name = 'send_message' THEN ex.user_id ELSE NULL END) as metric,\
                        COUNT(CASE WHEN event_name = 'send_message' THEN ex.user_id ELSE NULL END) / COUNT(DISTINCT ex.user_id) as average \
                FROM experiments ex \
                    JOIN users u ON ex.user_id = u.user_id \
                    JOIN events e on ex.user_id = e.user_id \
                    AND ex.experiment = 'publisher_update' \
                    AND e.occurred_at >= ex.occurred_at \
                    AND e.occurred_at < '2014-07' \
                    AND e.event_type = 'engagement' \
                    AND u.activated_at NOT LIKE '%2014-06%' \
                GROUP BY experiment_group"
    
    mod2_df = pd.read_sql(query, connection)
    mod2_df​
  • 최종 데이터

 

  • 그래프
    ax3 = sns.barplot(data=mod2_df, x='experiment_group',y='average')
    for p in ax3.patches:
        ax3.annotate(p.get_height(), (p.get_x()+p.get_width()/2, p.get_height()),
                    fontsize=12,color='red', ha='center',va='center', textcoords='offset points')​

 

분석4 결과

  • control group에서 6월에 가입한 user들을 제외했더니 2.6 -> 2.9로 Average Message Sent 수가 증가했고, 실험군과 대조군의 차이는 적어짐.
  • 차이가 적어졌지만 여전히 test group의 평균 메시지 전송수가 더 많다.

 

분석 결과 요약

  • 실험 기간 동안 실험군이 대조군 대비 유저 당 평균 0.8회 더 많이, 0.6일 더 자주 로그인 함 (분석2)
  • 실험 기간 동안 신규로 가입한 유저들이 모든 대조군으로 들어가는 오류가 있었다. (분석3)
  • 실험 기간 동안 신규로 가입한 유저(2014-06 가입)들을 제외하고, 기존 유저들의 데이터만 분석했을 때에도 실험 기간 동안 실험군의 메시지 평균 전송횟수는 4.07회, 대조군 2.91회로 그 차이는 줄어들었으나 여전히 실험군에서 메시지 전송량이 많음을 확인 (분석4) 

 

출처

 

 

💟 분석에 사용할 데이터와 분석할 문제에 대한 설명은 아래 링크에서 확인할 수 있다.

https://mode.com/sql-tutorial/validating-ab-test-results

 

Validating A/B Test Results | SQL Analytics Training - Mode

In this lesson we'll cover: Before starting, be sure to read the overview to learn a bit about Yammer as a company. Yammer not only develops new features, but is continuously looking for ways to improving existing ones. Like many software companies, Yammer

mode.com

💟자세한 MySQL 코드https://github.com/JeeyeonKim00/TIL/blob/30e03bfa7049ab0e646680a94a3cb9aa0cd409fd/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A6%AC%EC%95%88/yammer%EB%B6%84%EC%84%9D/3_AB_test.ipynb

 

💟데이터리안 강의

 

[지금 무료] [백문이불여일타] 데이터 분석을 위한 SQL 실전편 (무료 미니 코스) 강의 - 인프런

인프런 누적 수강생 10,000명 이상, 풍부한 온/오프라인 강의 경험을 가진 데이터리안의 SQL 실무 강의. SQL은 실무에서 어떻게 활용되고 있을까요? Microsoft의 Yammer 서비스의 실제 데이터를 이용하여

www.inflearn.com