Data Analysis/- Case Study

[Yammer/Mode] A/B ํ…Œ์ŠคํŠธ ๋ถ„์„ : 1๏ธโƒฃ SQL ์ฟผ๋ฆฌ ๋œฏ์–ด๋ณด๊ธฐ

์Šค์œ„๋ฏผ 2025. 4. 8. 12:21

Yammer ๋ถ„์„ ํ”„๋กœ์ ํŠธ

"์•ผ๋จธ(Yammer)"๋Š” ๋งˆ์ดํฌ๋กœ์†Œํ”„ํŠธ ์‚ฐํ•˜์˜ ๊ธฐ์—…์šฉ ์†Œ์…œ ๋„คํŠธ์›Œํฌ ์„œ๋น„์Šค ํšŒ์‚ฌ์ž…๋‹ˆ๋‹ค. Mode์—์„œ ๊ฐ€์ƒ์˜ ์•ผ๋จธ์‚ฌ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ™œ์šฉํ•˜์—ฌ ํ˜„์—…๊ณผ ์œ ์‚ฌํ•œ ๋ฌธ์ œ ์ƒํ™ฉ์„ ๊ฐ€์ •ํ•˜๊ณ  ํ•ด๊ฒฐํ•˜๋Š” ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

 

 

A/B test ์ƒํ™ฉ

์•ผ๋จธ์‚ฌ์—์„œ ์ƒˆ๋กœ์šด ํฌ์ŠคํŒ…(publisher) ๊ธฐ๋Šฅ์„ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•ด A/B test๋ฅผ 6์›” 1์ผ๋ถ€ํ„ฐ 6์›” 30์ผ๊นŒ์ง€ ์ง„ํ–‰ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
์•ผ๋จธ์‚ฌ์— ๋กœ๊ทธ์ธํ•œ ์ผ๋ถ€ ์‚ฌ์šฉ์ž๋“ค์€ ๊ธฐ์กด ๋ฒ„์ „(๋Œ€์กฐ๊ตฐ)์„ ๋ณด์•˜๊ณ , ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž๋“ค์€ ์ƒˆ๋กœ์šด ๋ฒ„์ „(์‹คํ—˜๊ตฐ)์„ ๋ณด์•˜์Šต๋‹ˆ๋‹ค.
7์›” 1์ผ A/B ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•œ ๊ฒฐ๊ณผ, ์‹คํ—˜๊ตฐ์ด ๋Œ€์กฐ๊ตฐ ๋Œ€๋น„ ํฌ์ŠคํŒ… ๊ฐœ์ˆ˜๊ฐ€ 50% ๋” ๋†’๋‹ค๋Š” ๊ฒƒ์„ ๋ฐœ๊ฒฌํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ๊ฒŒ์‹œ ํ™œ๋™์ด ํฌ๊ฒŒ ์ฆ๊ฐ€ํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

โญ๏ธ ์ด๋ฒˆ ๊ธ€์„ ํ†ตํ•ด ์•ผ๋จธ์‚ฌ์—์„œ ์ง„ํ–‰ํ•œ A/B test๊ฐ€ ํŽธํ–ฅ๊ณผ ์˜ค๋ฅ˜์—†์ด ์ œ๋Œ€๋กœ ์ง„ํ–‰๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด SQL ์ฝ”๋“œ๋ฅผ ๋œฏ์–ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค!

 

 

A/B test ๊ฒฐ๊ณผ ํ™•์ธ

 

 

 

์ฟผ๋ฆฌ ํ™•์ธ

 

โฌ‡๏ธ Yammer Analyst(๊ฐ€์ƒ)๊ฐ€ ์ž‘์„ฑํ•œ A/B test ๊ฒฐ๊ณผ ์ถ”์ถœ ์ฟผ๋ฆฌ

๋”๋ณด๊ธฐ
-- cte c์™€ ์ •๊ทœ๋ถ„ํฌํ‘œ ํ…Œ์ด๋ธ” ์กฐ์ธํ•ด์„œ ํ†ต๊ณ„์  ๊ฒ€์ • ์ง„ํ–‰
-- ์ง€ํ‘œ ์ฐจ์ด, ์ง€ํ‘œ ํ–ฅ์ƒ๋„, t_stat, p_value ๊ตฌํ•˜๊ธฐ
SELECT c.experiment,
       c.experiment_group,
       c.users,
       c.total_treated_users,
       ROUND(c.users/c.total_treated_users,4) AS treatment_percent,
       c.total,
       ROUND(c.average,4)::FLOAT AS average,
       ROUND(c.average - c.control_average,4) AS rate_difference,
       ROUND((c.average - c.control_average)/c.control_average,4) AS rate_lift,
       ROUND(c.stdev,4) AS stdev,
       ROUND((c.average - c.control_average) /
          SQRT((c.variance/c.users) + (c.control_variance/c.control_users))
        ,4) AS t_stat,
       (1 - COALESCE(nd.value,1))*2 AS p_value
  FROM (
  -- cte c : ..?
SELECT *,
       MAX(CASE WHEN b.experiment_group = 'control_group' THEN b.users ELSE NULL END) OVER () AS control_users,
       MAX(CASE WHEN b.experiment_group = 'control_group' THEN b.average ELSE NULL END) OVER () AS control_average,
       MAX(CASE WHEN b.experiment_group = 'control_group' THEN b.total ELSE NULL END) OVER () AS control_total,
       MAX(CASE WHEN b.experiment_group = 'control_group' THEN b.variance ELSE NULL END) OVER () AS control_variance,
       MAX(CASE WHEN b.experiment_group = 'control_group' THEN b.stdev ELSE NULL END) OVER () AS control_stdev,
       SUM(b.users) OVER () AS total_treated_users
  FROM (
  -- cte b : ๋Œ€์กฐ/์‹คํ—˜์ง‘๋‹จ์˜ ๋ชฉํ‘œ์ง€ํ‘œ์˜ ํ‰๊ท , ์ „์ฒด ์ˆ˜, ํ‘œ์ค€ํŽธ์ฐจ, ๋ถ„์‚ฐ ๊ตฌํ•˜๊ธฐ
SELECT a.experiment,
       a.experiment_group,
       COUNT(a.user_id) AS users,
       AVG(a.metric) AS average,
       SUM(a.metric) AS total,
       STDDEV(a.metric) AS stdev,
       VARIANCE(a.metric) AS variance
  FROM (
  -- cte a : ์‹คํ—˜๊ตฐ/๋Œ€์กฐ๊ตฐ ์œ ์ €์˜ send_message countํ•˜๊ธฐ : ๋ชฉํ‘œ metric
SELECT ex.experiment,
       ex.experiment_group,
       ex.occurred_at AS treatment_start,
       u.user_id,
       u.activated_at,
       COUNT(CASE WHEN e.event_name = 'send_message' THEN e.user_id ELSE NULL END) AS metric
  FROM (
  -- cte ex : ์‹คํ—˜ publisher_update ํ…Œ์ด๋ธ” ์ƒ์„ฑ
  SELECT user_id,
               experiment,
               experiment_group,
               occurred_at
          FROM tutorial.yammer_experiments
         WHERE experiment = 'publisher_update'
       ) ex
  -- ex
  JOIN tutorial.yammer_users u
    ON u.user_id = ex.user_id
  JOIN tutorial.yammer_events e
    ON e.user_id = ex.user_id
   AND e.occurred_at >= ex.occurred_at
   AND e.occurred_at < '2014-07-01'
   AND e.event_type = 'engagement'
 GROUP BY 1,2,3,4,5
       ) a
  -- a
 GROUP BY 1,2
       ) b
  -- b
       ) c
  -- c
  LEFT JOIN benn.normal_distribution nd
    ON nd.score = ABS(ROUND((c.average - c.control_average)/SQRT((c.variance/c.users) + (c.control_variance/c.control_users)),3))

 

์ฟผ๋ฆฌ ๋œฏ์–ด๋ณด๊ธฐ

์œ„ ์ฝ”๋“œ๋Š” ์„œ๋ธŒ์ฟผ๋ฆฌ๊ฐ€ ๋งŽ์•„์„œ ๋‹จ๊ณ„๋ณ„๋กœ ํ•œ๋ฒˆ ์ฐจ๊ทผ์ฐจ๊ทผ ๋œฏ์–ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค!

โ–ช๏ธ CTE ex :

๋”๋ณด๊ธฐ
with ex as (
SELECT user_id,
               experiment,
               experiment_group,
               occurred_at
          FROM tutorial.yammer_experiments
         WHERE experiment = 'publisher_update'
       )

 

ํŒ€์—์„œ ๋‹ค์ˆ˜์˜ ์‹คํ—˜์ด ์ง„ํ–‰๋ ํ…Œ๋‹ˆ ๊ทธ ์ค‘ 'publisher_update' ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค

 

โ–ช๏ธ CTE a : ์‹คํ—˜ ๊ธฐ๊ฐ„ ๋‚ด์— ์‹คํ—˜/๋Œ€์กฐ์ง‘๋‹จ์ด ๋ชฉํ‘œ์ง€ํ‘œ์ธ 'send_message'๋ฅผ ํ•œ ์ˆ˜๋ฅผ countํ•˜๊ธฐ

๋”๋ณด๊ธฐ
a as (
SELECT ex.experiment,
       ex.experiment_group,
       ex.occurred_at AS treatment_start,
       u.user_id,
       u.activated_at,
       COUNT(CASE WHEN e.event_name = 'send_message' THEN e.user_id ELSE NULL END) AS metric
  FROM ex
  JOIN tutorial.yammer_users u
    ON u.user_id = ex.user_id
  JOIN tutorial.yammer_events e
    ON e.user_id = ex.user_id
   AND e.occurred_at >= ex.occurred_at
   AND e.occurred_at < '2014-07-01'
   AND e.event_type = 'engagement'
 GROUP BY 1,2,3,4,5
       )

 

- yammer_events(์œ ์ € ํ–‰๋™ ๋กœ๊ทธ ํ…Œ์ด๋ธ”), yammer_users(์œ ์ € ๊ณ„์ • ํ…Œ์ด๋ธ”)์„ ์กฐ์ธํ•œ๋‹ค.

- ์‹คํ—˜ ์ข…๋ฅ˜ (experiment), ์‹คํ—˜/๋Œ€์กฐ ์ง‘๋‹จ (experiment_group), ์‹คํ—˜ ์‹œ์ž‘ ๊ธฐ๊ฐ„ (ex.occurred_at), ์œ ์ € ID (user_id), ๊ณ„์ • ํ™œ์„ฑํ™” ์‹œ๊ฐ„(activated_at)์„ ๊ธฐ์ค€์œผ๋กœ ๊ทธ๋ฃนํ™”ํ•œ๋‹ค.

- ์œ ์ €๋ณ„๋กœ 'send_message'(ํฌ์ŠคํŒ…)์˜ ํ–‰๋™์„ ํ•œ ์ˆ˜๋ฅผ countํ•œ๋‹ค. -> ์‹คํ—˜์˜ ๋ชฉํ‘œ์ง€ํ‘œ

 

์กฐ๊ฑด

- e.occurred_at (ํฌ์ŠคํŒ…ํ•œ ์‹œ๊ฐ„)์ด ex.occurred_at (์‹คํ—˜/๋Œ€์กฐ์ง‘๋‹จ์— ์ฒ˜์น˜๋ฐ›์€ ์‹œ๊ฐ„) ์ดํ›„์— ๋ฐœ์ƒ

- 2014๋…„ 7์›” 1์ผ ์ „์˜ ํ–‰๋™ ๋กœ๊ทธ๋ฅผ ํ•„ํ„ฐ๋ง (ex.occurred_at์€ 2014๋…„ 6์›” 1์ผ๋ถ€ํ„ฐ 30์ผ๊นŒ์ง€ ์ง„ํ–‰)

 

โ–ช๏ธ CTE b : ๋Œ€์กฐ์ง‘๋‹จ / ์‹คํ—˜์ง‘๋‹จ์˜ ๋ชฉํ‘œ์ง€ํ‘œ ๋น„๊ตํ•˜๊ธฐ

๋”๋ณด๊ธฐ
b as (
SELECT a.experiment,
       a.experiment_group,
       COUNT(a.user_id) AS users,
       AVG(a.metric) AS average,
       SUM(a.metric) AS total,
       STDDEV(a.metric) AS stdev,
       VARIANCE(a.metric) AS variance
 FROM a
 GROUP BY 1,2
       )

- ์‹คํ—˜/๋Œ€์กฐ์ง‘๋‹จ ์œ ์ € ์ˆ˜ / ์œ ์ €๋ณ„๋กœ ํฌ์ŠคํŒ…(๋ชฉํ‘œ์ง€ํ‘œ)์„ ํ•œ ํšŸ์ˆ˜์˜ ํ‰๊ท  / ์ง‘๋‹จ๋ณ„ ์ „์ฒด ํฌ์ŠคํŒ… ์ˆ˜ / ์ง‘๋‹จ๋ณ„ ํฌ์ŠคํŒ… ํ‘œ์ค€ํŽธ์ฐจ & ๋ถ„์‚ฐ

์ค‘๊ฐ„ ๊ฒฐ๊ณผ ํ™•์ธ

โ–ช๏ธ CTE c : 

๋”๋ณด๊ธฐ
c as (
SELECT *,
       MAX(CASE WHEN b.experiment_group = 'control_group' THEN b.users ELSE NULL END) OVER () AS control_users,
       MAX(CASE WHEN b.experiment_group = 'control_group' THEN b.average ELSE NULL END) OVER () AS control_average,
       MAX(CASE WHEN b.experiment_group = 'control_group' THEN b.total ELSE NULL END) OVER () AS control_total,
       MAX(CASE WHEN b.experiment_group = 'control_group' THEN b.variance ELSE NULL END) OVER () AS control_variance,
       MAX(CASE WHEN b.experiment_group = 'control_group' THEN b.stdev ELSE NULL END) OVER () AS control_stdev,
       SUM(b.users) OVER () AS total_treated_users
  FROM b)

- ๋Œ€์กฐ์ง‘๋‹จ์˜ ์œ ์ € ์ˆ˜, ํฌ์ŠคํŒ… ์ˆ˜ ํ‰๊ท , ํฌ์ŠคํŒ… ์ „์ฒด ์ˆ˜, ํฌ์ŠคํŒ… ๋ถ„์‚ฐ & ํ‘œ์ค€ํŽธ์ฐจ๋ฅผ ๊ตฌํ•ด์ค€๋‹ค ..? (์™œ ํ–ˆ๋Š”์ง€ ๋ชจ๋ฅด๊ฒ ์Œ)

- + ์ „์ฒด ์‹คํ—˜์— ์ฐธ๊ฐ€ํ•œ ์œ ์ € ์ˆ˜๋ฅผ ๊ตฌํ•ด์ค€๋‹ค

 

โ–ช๏ธ ์ตœ์ข… select ๋ฌธ : t-ํ†ต๊ณ„๋Ÿ‰์œผ๋กœ ํ†ต๊ณ„์  ์œ ์˜์„ฑ ๊ฒ€์ฆํ•˜๊ธฐ

๋”๋ณด๊ธฐ
SELECT c.experiment,
       c.experiment_group,
       c.users,
       c.total_treated_users,
       ROUND(c.users/c.total_treated_users,4) AS treatment_percent,
       c.total,
       ROUND(c.average,4)::FLOAT AS average,
       ROUND(c.average - c.control_average,4) AS rate_difference,
       ROUND((c.average - c.control_average)/c.control_average,4) AS rate_lift,
       ROUND(c.stdev,4) AS stdev,
       ROUND((c.average - c.control_average) /
          SQRT((c.variance/c.users) + (c.control_variance/c.control_users))
        ,4) AS t_stat, -- t-test
       (1 - COALESCE(nd.value,1))*2 AS p_value
  FROM c
  LEFT JOIN benn.normal_distribution nd -- t-test์˜ p-value ํ™•์ธ
    ON nd.score = ABS(ROUND((c.average - c.control_average)/SQRT((c.variance/c.users) + (c.control_variance/c.control_users)),3))

- ๋Œ€์กฐ๊ตฐ ๋Œ€๋น„ ์‹คํ—˜๊ตฐ์˜ ์ง€ํ‘œ๊ฐ€ ์–ผ๋งˆ๋‚˜ ๋ณ€ํ™”๊ฐ€ ์žˆ์—ˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค -> rate_difference, rate_lift

- two-sample t-test (๋‘์ง‘๋‹จ์˜ ํ‰๊ท  ๋น„๊ต)๋ฅผ ์ง„ํ–‰ํ•œ๋‹ค. -> ๋Œ€์กฐ์ง‘๋‹จ๊ณผ ์‹คํ—˜์ง‘๋‹จ์˜ ๋ชฉํ‘œ์ง€ํ‘œ์˜ ํ‰๊ท  ์ฐจ์ด๊ฐ€ ์œ ์˜๋ฏธํ•œ์ง€ ๊ฒ€์ฆ

two-sample t-test ์ˆ˜์‹ ์ฐธ๊ณ 

- t-test์˜ p-value๋ฅผ ๊ณ„์‚ฐํ•œ๋‹ค

-> t๋ถ„ํฌ๋Š” z๋ถ„ํฌ์™€ ์œ ์‚ฌํ•˜๊ธฐ ๋•Œ๋ฌธ์— mode์˜ benn.normal_distribution (์ •๊ทœ๋ถ„ํฌ ํ…Œ์ด๋ธ”)์„ ๊ฐ€์ ธ์™€์„œ t-๊ฐ’์˜ ๋ˆ„์ ํ™•๋ฅ (nd.value)๋ฅผ ๊ตฌํ•ด์ค€๋‹ค.
-> ์–‘์ธก๊ฒ€์ •์ด๊ธฐ ๋•Œ๋ฌธ์— (1 - nd.value) ์— 2๋ฅผ ๊ณฑํ•ด์ค˜์„œ p-value๋ฅผ ๊ตฌํ•œ๋‹ค.

 

 

๊ฒฐ๊ณผ ํ•ด์„

 

์ฟผ๋ฆฌ๋ฅผ ๋œฏ์–ด๋ณด๋ฉด์„œ SQL์„ ํ™œ์šฉํ•ด์„œ A/B test์˜ ๊ฒฐ๊ณผ๋ฅผ ๋„์ถœํ•˜๊ณ  ํ†ต๊ณ„์  ๊ฒ€์ •์„ ์ง„ํ–‰ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณผ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ์ฟผ๋ฆฌ๋ฅผ ํ™•์ธํ•ด๋ณด๋ฉด ํ•ด๋‹น A/B test์—์„œ ๋‹ค์ˆ˜์˜ ์˜ค๋ฅ˜๊ฐ€ ๋ฒ”ํ•ด์ง„ ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ ํฌ์ŠคํŠธ์—์„œ๋Š” ์–ด๋–ค ์˜ค๋ฅ˜๊ฐ€ ๋ฒ”ํ•ด์กŒ๋Š”์ง€ ๊ทธ๋ฆฌ๊ณ  ์–ด๋–ป๊ฒŒ ๊ณ ์ณ์•ผ ์œ ์˜๋ฏธํ•œ A/B test ๊ฒฐ๊ณผ๋ฅผ ๋„์ถœํ•  ์ˆ˜ ์žˆ์„์ง€ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค!