24. ์‹คํ—˜ ์„ค๊ณ„ (Experimental Design)

24. ์‹คํ—˜ ์„ค๊ณ„ (Experimental Design)

์ด์ „: ๋น„๋ชจ์ˆ˜ ํ†ต๊ณ„ | ๋‹ค์Œ: ์‹ค์ „ ํ”„๋กœ์ ํŠธ

๊ฐœ์š”

์‹คํ—˜ ์„ค๊ณ„๋Š” ์ธ๊ณผ๊ด€๊ณ„๋ฅผ ์ถ”๋ก ํ•˜๊ธฐ ์œ„ํ•œ ์ฒด๊ณ„์ ์ธ ๋ฐฉ๋ฒ•๋ก ์ž…๋‹ˆ๋‹ค. ์ด ์žฅ์—์„œ๋Š” ์‹คํ—˜ ์„ค๊ณ„์˜ ๊ธฐ๋ณธ ์›๋ฆฌ, A/B ํ…Œ์ŠคํŠธ, ๊ฒ€์ •๋ ฅ ๋ถ„์„์„ ํ†ตํ•œ ํ‘œ๋ณธ ํฌ๊ธฐ ๊ฒฐ์ •, ๊ทธ๋ฆฌ๊ณ  ์ˆœ์ฐจ์  ๊ฒ€์ • ๋ฐฉ๋ฒ•์„ ํ•™์Šตํ•ฉ๋‹ˆ๋‹ค.


1. ์‹คํ—˜ ์„ค๊ณ„์˜ ๊ธฐ๋ณธ ์›๋ฆฌ

1.1 ์„ธ ๊ฐ€์ง€ ํ•ต์‹ฌ ์›๋ฆฌ

import numpy as np
import pandas as pd
import scipy.stats as stats
import matplotlib.pyplot as plt
from scipy.stats import norm, t

np.random.seed(42)

def experimental_design_principles():
    """์‹คํ—˜ ์„ค๊ณ„์˜ ์„ธ ๊ฐ€์ง€ ํ•ต์‹ฌ ์›๋ฆฌ"""
    print("""
    =================================================
    ์‹คํ—˜ ์„ค๊ณ„์˜ ์„ธ ๊ฐ€์ง€ ํ•ต์‹ฌ ์›๋ฆฌ
    =================================================

    1. ๋ฌด์ž‘์œ„ํ™” (Randomization)
    โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    - ํ”ผํ—˜์ž๋ฅผ ์ฒ˜๋ฆฌ๊ตฐ์— ๋ฌด์ž‘์œ„๋กœ ๋ฐฐ์ •
    - ๊ต๋ž€๋ณ€์ˆ˜์˜ ์˜ํ–ฅ์„ ๊ท ๋“ฑํ•˜๊ฒŒ ๋ถ„๋ฐฐ
    - ์ธ๊ณผ๊ด€๊ณ„ ์ถ”๋ก ์˜ ๊ธฐ์ดˆ

    ์˜ˆ์‹œ:
    - ๋™์ „ ๋˜์ง€๊ธฐ๋กœ A/B ๊ทธ๋ฃน ๋ฐฐ์ •
    - ์ปดํ“จํ„ฐ ์ƒ์„ฑ ๋‚œ์ˆ˜ ์‚ฌ์šฉ
    - ๋ธ”๋ก ๋ฌด์ž‘์œ„ํ™” (์ธตํ™” ํ›„ ๋ฌด์ž‘์œ„)

    2. ๋ฐ˜๋ณต (Replication)
    โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    - ์ถฉ๋ถ„ํ•œ ์ˆ˜์˜ ๋…๋ฆฝ์  ๊ด€์ธก
    - ํ†ต๊ณ„์  ๊ฒ€์ •๋ ฅ ํ™•๋ณด
    - ๋ณ€๋™์„ฑ ์ถ”์ • ๊ฐ€๋Šฅ

    ๊ณ ๋ ค์‚ฌํ•ญ:
    - ํ‘œ๋ณธ ํฌ๊ธฐ ๊ณ„์‚ฐ (๊ฒ€์ •๋ ฅ ๋ถ„์„)
    - ๋น„์šฉ ๋Œ€๋น„ ํšจ๊ณผ
    - ์‹ค์šฉ์  ์ œ์•ฝ

    3. ๋ธ”๋กœํ‚น (Blocking)
    โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    - ์•Œ๋ ค์ง„ ๋ณ€๋™ ์š”์ธ์œผ๋กœ ํ”ผํ—˜์ž ๊ทธ๋ฃนํ™”
    - ๊ทธ๋ฃน ๋‚ด์—์„œ ๋ฌด์ž‘์œ„ ๋ฐฐ์ •
    - ์˜ค์ฐจ ๊ฐ์†Œ, ๊ฒ€์ •๋ ฅ ํ–ฅ์ƒ

    ์˜ˆ์‹œ:
    - ์„ฑ๋ณ„๋กœ ๋ธ”๋ก โ†’ ๊ฐ ๋ธ”๋ก ๋‚ด ๋ฌด์ž‘์œ„ ๋ฐฐ์ •
    - ์—ฐ๋ น๋Œ€๋กœ ์ธตํ™”
    - ์ง€์—ญ, ์‹œ๊ฐ„๋Œ€ ๋“ฑ

    =================================================
    ์ถ”๊ฐ€ ์›๋ฆฌ
    =================================================

    - ํ†ต์ œ (Control): ๋Œ€์กฐ๊ตฐ ํฌํ•จ
    - ๋งน๊ฒ€ (Blinding): ๋‹จ์ผ/์ด์ค‘ ๋งน๊ฒ€
    - ๊ท ํ˜• (Balance): ๊ทธ๋ฃน ๊ฐ„ ๊ท ๋“ฑ ๋ฐฐ์ •
    """)

experimental_design_principles()

1.2 ๋ฌด์ž‘์œ„ํ™” ๊ตฌํ˜„

def randomize_participants(participants, n_groups=2, method='simple', block_var=None):
    """
    ํ”ผํ—˜์ž ๋ฌด์ž‘์œ„ํ™”

    Parameters:
    -----------
    participants : DataFrame
        ํ”ผํ—˜์ž ์ •๋ณด
    n_groups : int
        ๊ทธ๋ฃน ์ˆ˜
    method : str
        'simple' - ๋‹จ์ˆœ ๋ฌด์ž‘์œ„
        'stratified' - ์ธตํ™” ๋ฌด์ž‘์œ„
    block_var : str
        ์ธตํ™” ๋ณ€์ˆ˜ (method='stratified'์ผ ๋•Œ)
    """
    n = len(participants)
    result = participants.copy()

    if method == 'simple':
        # ๋‹จ์ˆœ ๋ฌด์ž‘์œ„ ๋ฐฐ์ •
        assignments = np.random.choice(range(n_groups), size=n)
        result['group'] = assignments

    elif method == 'stratified' and block_var is not None:
        # ์ธตํ™” ๋ฌด์ž‘์œ„ ๋ฐฐ์ •
        result['group'] = -1
        for block_value in participants[block_var].unique():
            mask = participants[block_var] == block_value
            block_n = mask.sum()
            assignments = np.random.choice(range(n_groups), size=block_n)
            result.loc[mask, 'group'] = assignments

    return result

# ์˜ˆ์‹œ: 100๋ช…์˜ ํ”ผํ—˜์ž
np.random.seed(42)
participants = pd.DataFrame({
    'id': range(100),
    'age': np.random.choice(['young', 'middle', 'old'], 100),
    'gender': np.random.choice(['M', 'F'], 100)
})

# ๋‹จ์ˆœ ๋ฌด์ž‘์œ„
simple_rand = randomize_participants(participants, n_groups=2, method='simple')

# ์ธตํ™” ๋ฌด์ž‘์œ„ (์„ฑ๋ณ„ ๊ธฐ์ค€)
stratified_rand = randomize_participants(participants, n_groups=2,
                                          method='stratified', block_var='gender')

print("=== ๋‹จ์ˆœ ๋ฌด์ž‘์œ„ํ™” ๊ฒฐ๊ณผ ===")
print(pd.crosstab(simple_rand['gender'], simple_rand['group']))

print("\n=== ์ธตํ™” ๋ฌด์ž‘์œ„ํ™” ๊ฒฐ๊ณผ (์„ฑ๋ณ„ ๊ธฐ์ค€) ===")
print(pd.crosstab(stratified_rand['gender'], stratified_rand['group']))

1.3 ์‹คํ—˜ ์„ค๊ณ„ ์œ ํ˜•

def experimental_design_types():
    """์ฃผ์š” ์‹คํ—˜ ์„ค๊ณ„ ์œ ํ˜•"""
    print("""
    =================================================
    ์‹คํ—˜ ์„ค๊ณ„ ์œ ํ˜•
    =================================================

    1. ์™„์ „ ๋ฌด์ž‘์œ„ ์„ค๊ณ„ (Completely Randomized Design)
       - ๊ฐ€์žฅ ๋‹จ์ˆœํ•œ ์„ค๊ณ„
       - ํ”ผํ—˜์ž๋ฅผ ์ฒ˜๋ฆฌ๊ตฐ์— ์™„์ „ ๋ฌด์ž‘์œ„ ๋ฐฐ์ •
       - ๋ถ„์„: ๋…๋ฆฝํ‘œ๋ณธ t-๊ฒ€์ •, ์ผ์› ANOVA

    2. ๋ฌด์ž‘์œ„ ๋ธ”๋ก ์„ค๊ณ„ (Randomized Block Design)
       - ๋ธ”๋ก ๋ณ€์ˆ˜๋กœ ์ธตํ™” ํ›„ ๋ฌด์ž‘์œ„ ๋ฐฐ์ •
       - ๊ฐ ๋ธ”๋ก ๋‚ด ๋ชจ๋“  ์ฒ˜๋ฆฌ ์ˆ˜์ค€ ํฌํ•จ
       - ๋ถ„์„: ์ด์› ANOVA (๋ธ”๋ก ํšจ๊ณผ ์ œ๊ฑฐ)

    3. ์š”์ธ ์„ค๊ณ„ (Factorial Design)
       - ์—ฌ๋Ÿฌ ์š”์ธ์˜ ์กฐํ•ฉ ํšจ๊ณผ ์—ฐ๊ตฌ
       - ์ƒํ˜ธ์ž‘์šฉ ํšจ๊ณผ ๊ฒ€์ถœ ๊ฐ€๋Šฅ
       - ๋ถ„์„: ๋‹ค์› ANOVA

    4. ๊ต์ฐจ ์„ค๊ณ„ (Crossover Design)
       - ํ”ผํ—˜์ž๊ฐ€ ๋ชจ๋“  ์ฒ˜๋ฆฌ๋ฅผ ์ˆœ์ฐจ์ ์œผ๋กœ ๋ฐ›์Œ
       - ๊ฐœ์ธ ๊ฐ„ ๋ณ€๋™ ํ†ต์ œ
       - ์ด์›” ํšจ๊ณผ ์ฃผ์˜

    5. ๋ถ„ํ• ๊ตฌ ์„ค๊ณ„ (Split-Plot Design)
       - ํ•œ ์š”์ธ์€ ์ „์ฒด์—, ๋‹ค๋ฅธ ์š”์ธ์€ ๋ถ€๋ถ„์— ์ ์šฉ
       - ๋†์—…, ๊ณตํ•™์—์„œ ํ”ํ•จ
    """)

experimental_design_types()

2. A/B ํ…Œ์ŠคํŠธ ์ด๋ก 

2.1 A/B ํ…Œ์ŠคํŠธ ๊ฐœ์š”

def ab_test_overview():
    """A/B ํ…Œ์ŠคํŠธ ๊ฐœ์š”"""
    print("""
    =================================================
    A/B ํ…Œ์ŠคํŠธ (A/B Testing)
    =================================================

    ์ •์˜:
    - ๋‘ ๊ฐ€์ง€ ๋ฒ„์ „(A, B)์˜ ํšจ๊ณผ๋ฅผ ๋น„๊ตํ•˜๋Š” ๋ฌด์ž‘์œ„ ๋Œ€์กฐ ์‹คํ—˜
    - ์›น/์•ฑ์—์„œ ๊ฐ€์žฅ ๋„๋ฆฌ ์‚ฌ์šฉ๋˜๋Š” ์‹คํ—˜ ๋ฐฉ๋ฒ•

    ์šฉ์–ด:
    - Control (A): ๊ธฐ์กด ๋ฒ„์ „ (๋Œ€์กฐ๊ตฐ)
    - Treatment (B): ์ƒˆ ๋ฒ„์ „ (์‹คํ—˜๊ตฐ)
    - ์ „ํ™˜์œจ (Conversion Rate): ๋ชฉํ‘œ ํ–‰๋™ ๋น„์œจ
    - ์ƒ์Šน๋ฅ  (Lift): (B - A) / A

    ํ”„๋กœ์„ธ์Šค:
    1. ๊ฐ€์„ค ์ˆ˜๋ฆฝ
    2. ๋ฉ”ํŠธ๋ฆญ ์ •์˜
    3. ํ‘œ๋ณธ ํฌ๊ธฐ ๊ณ„์‚ฐ
    4. ์‹คํ—˜ ์‹คํ–‰
    5. ํ†ต๊ณ„ ๋ถ„์„
    6. ์˜์‚ฌ๊ฒฐ์ •

    ์ฃผ์˜์‚ฌํ•ญ:
    - ๋‹จ์œ„์˜ ์ผ๊ด€์„ฑ (์‚ฌ์šฉ์ž vs ์„ธ์…˜ vs ํŽ˜์ด์ง€๋ทฐ)
    - ์‹คํ—˜ ๊ธฐ๊ฐ„ (์ตœ์†Œ 1-2์ฃผ, ์š”์ผ ํšจ๊ณผ ๊ณ ๋ ค)
    - ๋‹ค์ค‘ ๋น„๊ต ๋ณด์ •
    - ๋„คํŠธ์›Œํฌ ํšจ๊ณผ (spillover)
    """)

ab_test_overview()

2.2 A/B ํ…Œ์ŠคํŠธ ๋ถ„์„

class ABTest:
    """A/B ํ…Œ์ŠคํŠธ ๋ถ„์„ ํด๋ž˜์Šค"""

    def __init__(self, control_visitors, control_conversions,
                 treatment_visitors, treatment_conversions):
        self.n_c = control_visitors
        self.x_c = control_conversions
        self.n_t = treatment_visitors
        self.x_t = treatment_conversions

        self.p_c = self.x_c / self.n_c
        self.p_t = self.x_t / self.n_t

    def z_test(self, alternative='two-sided'):
        """๋‘ ๋น„์œจ์˜ Z-๊ฒ€์ •"""
        # ํ†ตํ•ฉ ๋น„์œจ
        p_pooled = (self.x_c + self.x_t) / (self.n_c + self.n_t)

        # ํ‘œ์ค€์˜ค์ฐจ
        se = np.sqrt(p_pooled * (1 - p_pooled) * (1/self.n_c + 1/self.n_t))

        # Z ํ†ต๊ณ„๋Ÿ‰
        z = (self.p_t - self.p_c) / se

        # p-value
        if alternative == 'two-sided':
            p_value = 2 * (1 - norm.cdf(abs(z)))
        elif alternative == 'greater':  # treatment > control
            p_value = 1 - norm.cdf(z)
        else:  # treatment < control
            p_value = norm.cdf(z)

        return z, p_value

    def confidence_interval(self, alpha=0.05):
        """์ฐจ์ด์˜ ์‹ ๋ขฐ๊ตฌ๊ฐ„"""
        diff = self.p_t - self.p_c

        # ๊ฐ ๋น„์œจ์˜ ๋ถ„์‚ฐ
        var_c = self.p_c * (1 - self.p_c) / self.n_c
        var_t = self.p_t * (1 - self.p_t) / self.n_t
        se = np.sqrt(var_c + var_t)

        z_crit = norm.ppf(1 - alpha/2)
        ci_lower = diff - z_crit * se
        ci_upper = diff + z_crit * se

        return diff, (ci_lower, ci_upper)

    def lift(self):
        """์ƒ์Šน๋ฅ  ๊ณ„์‚ฐ"""
        if self.p_c == 0:
            return np.inf
        return (self.p_t - self.p_c) / self.p_c

    def summary(self):
        """๊ฒฐ๊ณผ ์š”์•ฝ"""
        print("=== A/B Test Summary ===")
        print(f"\nControl:   {self.x_c:,}/{self.n_c:,} = {self.p_c:.4f} ({self.p_c*100:.2f}%)")
        print(f"Treatment: {self.x_t:,}/{self.n_t:,} = {self.p_t:.4f} ({self.p_t*100:.2f}%)")

        z, p_value = self.z_test()
        diff, ci = self.confidence_interval()
        lift = self.lift()

        print(f"\n์ฐจ์ด: {diff:.4f} ({diff*100:.2f}%p)")
        print(f"์ƒ์Šน๋ฅ : {lift*100:.2f}%")
        print(f"95% CI: ({ci[0]*100:.2f}%p, {ci[1]*100:.2f}%p)")
        print(f"\nZ ํ†ต๊ณ„๋Ÿ‰: {z:.3f}")
        print(f"p-value: {p_value:.4f}")

        if p_value < 0.05:
            print("\n๊ฒฐ๋ก : ํ†ต๊ณ„์ ์œผ๋กœ ์œ ์˜ํ•œ ์ฐจ์ด ์žˆ์Œ (p < 0.05)")
            if diff > 0:
                print("Treatment๊ฐ€ Control๋ณด๋‹ค ์œ ์˜ํ•˜๊ฒŒ ๋†’์Œ")
            else:
                print("Treatment๊ฐ€ Control๋ณด๋‹ค ์œ ์˜ํ•˜๊ฒŒ ๋‚ฎ์Œ")
        else:
            print("\n๊ฒฐ๋ก : ํ†ต๊ณ„์ ์œผ๋กœ ์œ ์˜ํ•œ ์ฐจ์ด ์—†์Œ (p >= 0.05)")


# ์˜ˆ์‹œ: ๋ฒ„ํŠผ ์ƒ‰์ƒ A/B ํ…Œ์ŠคํŠธ
ab_test = ABTest(
    control_visitors=10000,
    control_conversions=350,
    treatment_visitors=10000,
    treatment_conversions=420
)
ab_test.summary()

# ์‹œ๊ฐํ™”
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

# ์ „ํ™˜์œจ ๋น„๊ต
ax = axes[0]
bars = ax.bar(['Control', 'Treatment'], [ab_test.p_c, ab_test.p_t], alpha=0.7)
ax.set_ylabel('์ „ํ™˜์œจ')
ax.set_title('A/B ํ…Œ์ŠคํŠธ: ์ „ํ™˜์œจ ๋น„๊ต')

# ์—๋Ÿฌ๋ฐ” ์ถ”๊ฐ€
se_c = np.sqrt(ab_test.p_c * (1 - ab_test.p_c) / ab_test.n_c)
se_t = np.sqrt(ab_test.p_t * (1 - ab_test.p_t) / ab_test.n_t)
ax.errorbar(['Control', 'Treatment'], [ab_test.p_c, ab_test.p_t],
            yerr=[1.96*se_c, 1.96*se_t], fmt='none', color='black', capsize=5)
ax.grid(True, alpha=0.3, axis='y')

# ์ฐจ์ด์˜ ์‹ ๋ขฐ๊ตฌ๊ฐ„
ax = axes[1]
diff, ci = ab_test.confidence_interval()
ax.errorbar([0], [diff], yerr=[[diff - ci[0]], [ci[1] - diff]],
            fmt='o', markersize=10, capsize=10, capthick=2)
ax.axhline(0, color='r', linestyle='--', label='์ฐจ์ด ์—†์Œ')
ax.set_xlim(-1, 1)
ax.set_ylabel('์ „ํ™˜์œจ ์ฐจ์ด')
ax.set_title(f'์ฐจ์ด์˜ 95% ์‹ ๋ขฐ๊ตฌ๊ฐ„\n({ci[0]:.4f}, {ci[1]:.4f})')
ax.set_xticks([])
ax.legend()
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

2.3 ๋ฒ ์ด์ง€์•ˆ A/B ํ…Œ์ŠคํŠธ

def bayesian_ab_test(n_c, x_c, n_t, x_t, alpha_prior=1, beta_prior=1, n_samples=100000):
    """
    ๋ฒ ์ด์ง€์•ˆ A/B ํ…Œ์ŠคํŠธ

    Beta ์‚ฌ์ „๋ถ„ํฌ๋ฅผ ์‚ฌ์šฉํ•œ ์ „ํ™˜์œจ ์ถ”์ •
    """
    # ์‚ฌํ›„๋ถ„ํฌ (Beta-Binomial conjugate)
    alpha_c = alpha_prior + x_c
    beta_c = beta_prior + n_c - x_c
    alpha_t = alpha_prior + x_t
    beta_t = beta_prior + n_t - x_t

    # ์‚ฌํ›„๋ถ„ํฌ์—์„œ ์ƒ˜ํ”Œ๋ง
    samples_c = np.random.beta(alpha_c, beta_c, n_samples)
    samples_t = np.random.beta(alpha_t, beta_t, n_samples)

    # P(Treatment > Control)
    prob_t_better = np.mean(samples_t > samples_c)

    # ๊ธฐ๋Œ€ ์ƒ์Šน๋ฅ 
    lift_samples = (samples_t - samples_c) / samples_c
    expected_lift = np.mean(lift_samples)
    lift_ci = np.percentile(lift_samples, [2.5, 97.5])

    print("=== ๋ฒ ์ด์ง€์•ˆ A/B ํ…Œ์ŠคํŠธ ===")
    print(f"\nP(Treatment > Control): {prob_t_better:.4f} ({prob_t_better*100:.1f}%)")
    print(f"๊ธฐ๋Œ€ ์ƒ์Šน๋ฅ : {expected_lift*100:.2f}%")
    print(f"์ƒ์Šน๋ฅ  95% CI: ({lift_ci[0]*100:.2f}%, {lift_ci[1]*100:.2f}%)")

    # ์˜์‚ฌ๊ฒฐ์ • ๊ธฐ์ค€
    print("\n์˜์‚ฌ๊ฒฐ์ •:")
    if prob_t_better > 0.95:
        print("  โ†’ Treatment ์ฑ„ํƒ ๊ถŒ์žฅ (P > 95%)")
    elif prob_t_better < 0.05:
        print("  โ†’ Control ์œ ์ง€ ๊ถŒ์žฅ (P < 5%)")
    else:
        print("  โ†’ ์ถ”๊ฐ€ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ํ•„์š”")

    # ์‹œ๊ฐํ™”
    fig, axes = plt.subplots(1, 3, figsize=(15, 4))

    # ์‚ฌํ›„๋ถ„ํฌ ๋น„๊ต
    ax = axes[0]
    x_range = np.linspace(0, 0.1, 200)
    ax.plot(x_range, stats.beta(alpha_c, beta_c).pdf(x_range), label='Control')
    ax.plot(x_range, stats.beta(alpha_t, beta_t).pdf(x_range), label='Treatment')
    ax.fill_between(x_range, stats.beta(alpha_c, beta_c).pdf(x_range), alpha=0.3)
    ax.fill_between(x_range, stats.beta(alpha_t, beta_t).pdf(x_range), alpha=0.3)
    ax.set_xlabel('์ „ํ™˜์œจ')
    ax.set_ylabel('๋ฐ€๋„')
    ax.set_title('์ „ํ™˜์œจ ์‚ฌํ›„๋ถ„ํฌ')
    ax.legend()
    ax.grid(True, alpha=0.3)

    # ์ฐจ์ด ๋ถ„ํฌ
    ax = axes[1]
    diff_samples = samples_t - samples_c
    ax.hist(diff_samples, bins=50, density=True, alpha=0.7, edgecolor='black')
    ax.axvline(0, color='r', linestyle='--', label='์ฐจ์ด ์—†์Œ')
    ax.axvline(np.mean(diff_samples), color='g', linestyle='-',
               label=f'ํ‰๊ท : {np.mean(diff_samples):.4f}')
    ax.set_xlabel('์ „ํ™˜์œจ ์ฐจ์ด (T - C)')
    ax.set_ylabel('๋ฐ€๋„')
    ax.set_title(f'์ฐจ์ด ์‚ฌํ›„๋ถ„ํฌ\nP(T>C)={prob_t_better:.3f}')
    ax.legend()
    ax.grid(True, alpha=0.3)

    # ์ƒ์Šน๋ฅ  ๋ถ„ํฌ
    ax = axes[2]
    lift_samples_clipped = np.clip(lift_samples, -1, 2)
    ax.hist(lift_samples_clipped, bins=50, density=True, alpha=0.7, edgecolor='black')
    ax.axvline(0, color='r', linestyle='--', label='0%')
    ax.axvline(expected_lift, color='g', linestyle='-',
               label=f'๊ธฐ๋Œ€๊ฐ’: {expected_lift*100:.1f}%')
    ax.set_xlabel('์ƒ์Šน๋ฅ ')
    ax.set_ylabel('๋ฐ€๋„')
    ax.set_title('์ƒ์Šน๋ฅ  ์‚ฌํ›„๋ถ„ํฌ')
    ax.legend()
    ax.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.show()

    return prob_t_better, expected_lift

# ๋ฒ ์ด์ง€์•ˆ ๋ถ„์„
prob_better, exp_lift = bayesian_ab_test(10000, 350, 10000, 420)

3. ํ‘œ๋ณธ ํฌ๊ธฐ ๊ฒฐ์ • (๊ฒ€์ •๋ ฅ ๋ถ„์„)

3.1 ๊ฒ€์ •๋ ฅ ๋ถ„์„ ๊ฐœ๋…

def power_analysis_concepts():
    """๊ฒ€์ •๋ ฅ ๋ถ„์„ ํ•ต์‹ฌ ๊ฐœ๋…"""
    print("""
    =================================================
    ๊ฒ€์ •๋ ฅ ๋ถ„์„ (Power Analysis)
    =================================================

    ๋„ค ๊ฐ€์ง€ ์š”์†Œ (ํ•˜๋‚˜๋ฅผ ๋‹ค๋ฅธ ์…‹์œผ๋กœ๋ถ€ํ„ฐ ๊ณ„์‚ฐ):
    โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    1. ํšจ๊ณผ ํฌ๊ธฐ (Effect Size)
       - ํƒ์ง€ํ•˜๊ณ ์ž ํ•˜๋Š” ์ตœ์†Œ ํšจ๊ณผ
       - ์˜ˆ: ์ „ํ™˜์œจ ์ฐจ์ด 0.02 (2%p)

    2. ์œ ์˜์ˆ˜์ค€ ฮฑ (Significance Level)
       - ์ œ1์ข… ์˜ค๋ฅ˜ ํ™•๋ฅ 
       - ์ผ๋ฐ˜์ ์œผ๋กœ 0.05

    3. ๊ฒ€์ •๋ ฅ 1-ฮฒ (Power)
       - ํšจ๊ณผ๊ฐ€ ์žˆ์„ ๋•Œ ํƒ์ง€ํ•  ํ™•๋ฅ 
       - ์ผ๋ฐ˜์ ์œผ๋กœ 0.80 (์ตœ์†Œ) ~ 0.90

    4. ํ‘œ๋ณธ ํฌ๊ธฐ n (Sample Size)
       - ํ•„์š”ํ•œ ๊ด€์ธก ์ˆ˜

    ๊ณ„์‚ฐ ํ๋ฆ„:
    โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    ํšจ๊ณผ ํฌ๊ธฐ + ฮฑ + (1-ฮฒ) โ†’ n (์‚ฌ์ „ ์„ค๊ณ„)
    n + ฮฑ + (1-ฮฒ) โ†’ ์ตœ์†Œ ํƒ์ง€ ๊ฐ€๋Šฅ ํšจ๊ณผ (๋ฏผ๊ฐ๋„ ๋ถ„์„)
    n + ฮฑ + ํšจ๊ณผ ํฌ๊ธฐ โ†’ ๋‹ฌ์„ฑ ๊ฒ€์ •๋ ฅ (์‚ฌํ›„ ๋ถ„์„)

    ๊ฒฝํ—˜ ๋ฒ•์น™:
    โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    - ๊ฒ€์ •๋ ฅ 80% ๋ฏธ๋งŒ: ๊ณผ์†Œ ๊ฒ€์ •๋ ฅ
    - ๊ฒ€์ •๋ ฅ 80-90%: ์ผ๋ฐ˜์  ๊ถŒ์žฅ
    - ๊ฒ€์ •๋ ฅ 90% ์ด์ƒ: ๊ณ ๊ฒ€์ •๋ ฅ ์—ฐ๊ตฌ
    """)

power_analysis_concepts()

3.2 ๋‘ ๋น„์œจ ๋น„๊ต์˜ ํ‘œ๋ณธ ํฌ๊ธฐ

def sample_size_two_proportions(p1, p2, alpha=0.05, power=0.80, ratio=1):
    """
    ๋‘ ๋น„์œจ ๋น„๊ต๋ฅผ ์œ„ํ•œ ํ‘œ๋ณธ ํฌ๊ธฐ ๊ณ„์‚ฐ

    Parameters:
    -----------
    p1 : float
        Control ์ „ํ™˜์œจ (๊ธฐ์ค€)
    p2 : float
        Treatment ์ „ํ™˜์œจ (๋ชฉํ‘œ)
    alpha : float
        ์œ ์˜์ˆ˜์ค€
    power : float
        ๊ฒ€์ •๋ ฅ
    ratio : float
        n2/n1 ๋น„์œจ (๊ธฐ๋ณธ๊ฐ’ 1 = ๋™์ผ ํฌ๊ธฐ)

    Returns:
    --------
    n1, n2 : int
        ๊ฐ ๊ทธ๋ฃน์˜ ํ•„์š” ํ‘œ๋ณธ ํฌ๊ธฐ
    """
    # ํšจ๊ณผ ํฌ๊ธฐ
    effect = abs(p2 - p1)
    p_pooled = (p1 + ratio * p2) / (1 + ratio)

    # Z ๊ฐ’
    z_alpha = norm.ppf(1 - alpha/2)  # ์–‘์ธก
    z_beta = norm.ppf(power)

    # ํ‘œ๋ณธ ํฌ๊ธฐ ๊ณต์‹
    numerator = (z_alpha * np.sqrt((1 + ratio) * p_pooled * (1 - p_pooled)) +
                 z_beta * np.sqrt(p1 * (1 - p1) + ratio * p2 * (1 - p2)))**2
    n1 = numerator / (effect**2 * ratio)
    n2 = n1 * ratio

    return int(np.ceil(n1)), int(np.ceil(n2))


def plot_sample_size_analysis(p1_base, effects, alpha=0.05, power=0.80):
    """ํšจ๊ณผ ํฌ๊ธฐ์— ๋”ฐ๋ฅธ ํ•„์š” ํ‘œ๋ณธ ํฌ๊ธฐ"""

    fig, axes = plt.subplots(1, 2, figsize=(14, 5))

    # ํšจ๊ณผ ํฌ๊ธฐ vs ํ‘œ๋ณธ ํฌ๊ธฐ
    ax = axes[0]
    sample_sizes = []
    for effect in effects:
        p2 = p1_base + effect
        n1, _ = sample_size_two_proportions(p1_base, p2, alpha, power)
        sample_sizes.append(n1)

    ax.plot(np.array(effects)*100, sample_sizes, 'bo-', linewidth=2)
    ax.set_xlabel('ํšจ๊ณผ ํฌ๊ธฐ (์ „ํ™˜์œจ ์ฐจ์ด %p)')
    ax.set_ylabel('๊ทธ๋ฃน๋‹น ํ•„์š” ํ‘œ๋ณธ ํฌ๊ธฐ')
    ax.set_title(f'ํšจ๊ณผ ํฌ๊ธฐ vs ํ‘œ๋ณธ ํฌ๊ธฐ\n(๊ธฐ์ค€ ์ „ํ™˜์œจ={p1_base:.1%}, ฮฑ={alpha}, power={power})')
    ax.grid(True, alpha=0.3)

    # ๋กœ๊ทธ ์Šค์ผ€์ผ
    ax.set_yscale('log')
    for i, (eff, n) in enumerate(zip(effects, sample_sizes)):
        ax.annotate(f'{n:,}', (eff*100, n), textcoords="offset points",
                    xytext=(0, 10), ha='center', fontsize=9)

    # ๊ฒ€์ •๋ ฅ vs ํ‘œ๋ณธ ํฌ๊ธฐ
    ax = axes[1]
    effect_fixed = 0.02  # 2%p ๊ณ ์ •
    p2_fixed = p1_base + effect_fixed
    powers = np.linspace(0.5, 0.95, 10)
    sample_sizes_power = []

    for pwr in powers:
        n1, _ = sample_size_two_proportions(p1_base, p2_fixed, alpha, pwr)
        sample_sizes_power.append(n1)

    ax.plot(powers*100, sample_sizes_power, 'go-', linewidth=2)
    ax.set_xlabel('๊ฒ€์ •๋ ฅ (%)')
    ax.set_ylabel('๊ทธ๋ฃน๋‹น ํ•„์š” ํ‘œ๋ณธ ํฌ๊ธฐ')
    ax.set_title(f'๊ฒ€์ •๋ ฅ vs ํ‘œ๋ณธ ํฌ๊ธฐ\n(ํšจ๊ณผ ํฌ๊ธฐ={effect_fixed:.1%}p)')
    ax.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.show()

# ์˜ˆ์‹œ
p1 = 0.05  # ๊ธฐ์ค€ ์ „ํ™˜์œจ 5%
effects = [0.005, 0.01, 0.015, 0.02, 0.025, 0.03]  # 0.5%p ~ 3%p

print("=== ํ‘œ๋ณธ ํฌ๊ธฐ ๊ณ„์‚ฐ ===")
print(f"๊ธฐ์ค€ ์ „ํ™˜์œจ: {p1:.1%}")
print(f"ฮฑ = 0.05, Power = 0.80")
print()
for effect in effects:
    p2 = p1 + effect
    n1, n2 = sample_size_two_proportions(p1, p2)
    print(f"ํšจ๊ณผ {effect*100:.1f}%p (์ƒ๋Œ€ {effect/p1*100:.0f}%): n1={n1:,}, n2={n2:,}, ์ด={n1+n2:,}")

plot_sample_size_analysis(p1, effects)

3.3 statsmodels ๊ฒ€์ •๋ ฅ ๋ถ„์„

from statsmodels.stats.power import TTestPower, NormalIndPower, tt_ind_solve_power
from statsmodels.stats.proportion import proportion_effectsize

def statsmodels_power_analysis():
    """statsmodels๋ฅผ ์‚ฌ์šฉํ•œ ๊ฒ€์ •๋ ฅ ๋ถ„์„"""

    # 1. t-๊ฒ€์ • ๊ฒ€์ •๋ ฅ ๋ถ„์„
    print("=== t-๊ฒ€์ • ๊ฒ€์ •๋ ฅ ๋ถ„์„ ===")

    # ํšจ๊ณผ ํฌ๊ธฐ ๊ณ„์‚ฐ (Cohen's d)
    # d = (ฮผ1 - ฮผ2) / ฯƒ
    mean_diff = 5
    std = 15
    d = mean_diff / std
    print(f"Cohen's d = {d:.3f}")

    # ํ•„์š” ํ‘œ๋ณธ ํฌ๊ธฐ
    power_analysis = TTestPower()
    n = power_analysis.solve_power(effect_size=d, alpha=0.05, power=0.80,
                                     alternative='two-sided')
    print(f"ํ•„์š” ํ‘œ๋ณธ ํฌ๊ธฐ (๊ฐ ๊ทธ๋ฃน): {int(np.ceil(n))}")

    # ๋‹ฌ์„ฑ ๊ฒ€์ •๋ ฅ
    achieved_power = power_analysis.power(effect_size=d, nobs=100, alpha=0.05,
                                           alternative='two-sided')
    print(f"n=100์ผ ๋•Œ ๊ฒ€์ •๋ ฅ: {achieved_power:.3f}")

    # 2. ๋น„์œจ ๊ฒ€์ • ๊ฒ€์ •๋ ฅ ๋ถ„์„
    print("\n=== ๋น„์œจ ๊ฒ€์ • ๊ฒ€์ •๋ ฅ ๋ถ„์„ ===")

    p1 = 0.05
    p2 = 0.07
    effect = proportion_effectsize(p1, p2)
    print(f"ํšจ๊ณผ ํฌ๊ธฐ (h): {effect:.3f}")

    # ํ•„์š” ํ‘œ๋ณธ ํฌ๊ธฐ
    power_prop = NormalIndPower()
    n_prop = power_prop.solve_power(effect_size=effect, alpha=0.05, power=0.80,
                                      alternative='two-sided', ratio=1)
    print(f"ํ•„์š” ํ‘œ๋ณธ ํฌ๊ธฐ (๊ฐ ๊ทธ๋ฃน): {int(np.ceil(n_prop))}")

    return n, n_prop

n_t, n_prop = statsmodels_power_analysis()

3.4 ๊ฒ€์ •๋ ฅ ๊ณก์„ 

def plot_power_curve(effect_sizes, n_per_group, alpha=0.05):
    """๊ฒ€์ •๋ ฅ ๊ณก์„  ์‹œ๊ฐํ™”"""

    power_analysis = NormalIndPower()

    fig, axes = plt.subplots(1, 2, figsize=(14, 5))

    # ํšจ๊ณผ ํฌ๊ธฐ vs ๊ฒ€์ •๋ ฅ (n ๊ณ ์ •)
    ax = axes[0]
    for n in n_per_group:
        powers = [power_analysis.power(effect_size=es, nobs=n, alpha=alpha,
                                        alternative='two-sided', ratio=1)
                  for es in effect_sizes]
        ax.plot(effect_sizes, powers, '-o', label=f'n={n}')

    ax.axhline(0.80, color='r', linestyle='--', alpha=0.5, label='Power=0.80')
    ax.set_xlabel('ํšจ๊ณผ ํฌ๊ธฐ (Cohen\'s h)')
    ax.set_ylabel('๊ฒ€์ •๋ ฅ')
    ax.set_title('๊ฒ€์ •๋ ฅ ๊ณก์„  (ํ‘œ๋ณธ ํฌ๊ธฐ๋ณ„)')
    ax.legend()
    ax.grid(True, alpha=0.3)
    ax.set_ylim(0, 1)

    # ํ‘œ๋ณธ ํฌ๊ธฐ vs ๊ฒ€์ •๋ ฅ (ํšจ๊ณผ ํฌ๊ธฐ ๊ณ ์ •)
    ax = axes[1]
    n_range = np.arange(50, 1001, 50)
    effect_fixed = [0.1, 0.2, 0.3, 0.5]

    for es in effect_fixed:
        powers = [power_analysis.power(effect_size=es, nobs=n, alpha=alpha,
                                        alternative='two-sided', ratio=1)
                  for n in n_range]
        ax.plot(n_range, powers, '-', label=f'h={es}')

    ax.axhline(0.80, color='r', linestyle='--', alpha=0.5, label='Power=0.80')
    ax.set_xlabel('๊ทธ๋ฃน๋‹น ํ‘œ๋ณธ ํฌ๊ธฐ')
    ax.set_ylabel('๊ฒ€์ •๋ ฅ')
    ax.set_title('๊ฒ€์ •๋ ฅ ๊ณก์„  (ํšจ๊ณผ ํฌ๊ธฐ๋ณ„)')
    ax.legend()
    ax.grid(True, alpha=0.3)
    ax.set_ylim(0, 1)

    plt.tight_layout()
    plt.show()

effect_sizes = np.linspace(0.05, 0.5, 20)
n_per_group = [50, 100, 200, 500]
plot_power_curve(effect_sizes, n_per_group)

4. ์ˆœ์ฐจ์  ๊ฒ€์ • (Sequential Testing)

4.1 ์™œ ์ˆœ์ฐจ์  ๊ฒ€์ •์ธ๊ฐ€?

def sequential_testing_motivation():
    """์ˆœ์ฐจ์  ๊ฒ€์ •์˜ ํ•„์š”์„ฑ"""
    print("""
    =================================================
    ์ˆœ์ฐจ์  ๊ฒ€์ • (Sequential Testing)
    =================================================

    ๋ฌธ์ œ: Peeking Problem
    โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    - A/B ํ…Œ์ŠคํŠธ ์ค‘๊ฐ„์— ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•˜๋ฉด ์ œ1์ข… ์˜ค๋ฅ˜์œจ ์ฆ๊ฐ€
    - ฮฑ=0.05๋กœ ์„ค๊ณ„ํ•ด๋„ 5๋ฒˆ ์ค‘๊ฐ„ ํ™•์ธ ์‹œ ์‹ค์ œ ์˜ค๋ฅ˜์œจ ~14%

    ์˜ˆ์‹œ:
    - 1ํšŒ ํ™•์ธ: ฮฑ = 0.05
    - 5ํšŒ ํ™•์ธ: ฮฑ โ‰ˆ 0.14
    - 10ํšŒ ํ™•์ธ: ฮฑ โ‰ˆ 0.19

    ํ•ด๊ฒฐ์ฑ…:
    โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    1. ๊ณ ์ • ํ‘œ๋ณธ ๊ฒ€์ •: ๋ฏธ๋ฆฌ ์ •ํ•œ n๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆผ
    2. ์ˆœ์ฐจ์  ๊ฒ€์ •: ์ค‘๊ฐ„ ํ™•์ธ์„ ํ—ˆ์šฉํ•˜๋˜ ๋ณด์ •
       - O'Brien-Fleming
       - Pocock
       - Alpha spending functions

    ์žฅ์ :
    - ํšจ๊ณผ๊ฐ€ ๋ช…ํ™•ํ•˜๋ฉด ์กฐ๊ธฐ ์ข…๋ฃŒ โ†’ ๋น„์šฉ/์‹œ๊ฐ„ ์ ˆ์•ฝ
    - ํšจ๊ณผ๊ฐ€ ์—†์œผ๋ฉด ๋น ๋ฅธ ์ข…๋ฃŒ
    - ํ†ต๊ณ„์  ํƒ€๋‹น์„ฑ ์œ ์ง€
    """)

sequential_testing_motivation()

4.2 Peeking ๋ฌธ์ œ ์‹œ๋ฎฌ๋ ˆ์ด์…˜

def simulate_peeking_problem(n_simulations=10000, n_total=1000, n_looks=5):
    """
    Peeking ๋ฌธ์ œ ์‹œ๋ฎฌ๋ ˆ์ด์…˜:
    ๊ท€๋ฌด๊ฐ€์„ค์ด ์ฐธ์ผ ๋•Œ (์‹ค์ œ ์ฐจ์ด ์—†์Œ) ์–ผ๋งˆ๋‚˜ ์ž์ฃผ ์œ ์˜ํ•˜๊ฒŒ ๋‚˜์˜ค๋Š”๊ฐ€
    """
    np.random.seed(42)
    alpha = 0.05

    # ์ค‘๊ฐ„ ํ™•์ธ ์‹œ์ 
    look_points = np.linspace(n_total // n_looks, n_total, n_looks).astype(int)

    false_positives_fixed = 0  # ๊ณ ์ • ํ‘œ๋ณธ (๋งˆ์ง€๋ง‰๋งŒ ํ™•์ธ)
    false_positives_peeking = 0  # ๋ชจ๋“  ์‹œ์  ํ™•์ธ

    for _ in range(n_simulations):
        # ๊ท€๋ฌด๊ฐ€์„ค ํ•˜์—์„œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ (๋‘ ๊ทธ๋ฃน ๋™์ผ)
        control = np.random.binomial(1, 0.1, n_total)
        treatment = np.random.binomial(1, 0.1, n_total)

        # Peeking: ๊ฐ ์‹œ์ ์—์„œ ๊ฒ€์ •
        for look in look_points:
            x_c = control[:look].sum()
            x_t = treatment[:look].sum()
            n = look

            # ๋น„์œจ
            p_c = x_c / n
            p_t = x_t / n
            p_pooled = (x_c + x_t) / (2 * n)

            if p_pooled > 0 and p_pooled < 1:
                se = np.sqrt(p_pooled * (1 - p_pooled) * 2 / n)
                z = (p_t - p_c) / se if se > 0 else 0
                p_value = 2 * (1 - norm.cdf(abs(z)))

                if p_value < alpha:
                    false_positives_peeking += 1
                    break  # ํ•œ ๋ฒˆ์ด๋ผ๋„ ์œ ์˜ํ•˜๋ฉด ์ข…๋ฃŒ

        # ๊ณ ์ • ํ‘œ๋ณธ: ๋งˆ์ง€๋ง‰๋งŒ ํ™•์ธ
        x_c = control.sum()
        x_t = treatment.sum()
        p_c = x_c / n_total
        p_t = x_t / n_total
        p_pooled = (x_c + x_t) / (2 * n_total)

        if p_pooled > 0 and p_pooled < 1:
            se = np.sqrt(p_pooled * (1 - p_pooled) * 2 / n_total)
            z = (p_t - p_c) / se if se > 0 else 0
            p_value = 2 * (1 - norm.cdf(abs(z)))

            if p_value < alpha:
                false_positives_fixed += 1

    fpr_fixed = false_positives_fixed / n_simulations
    fpr_peeking = false_positives_peeking / n_simulations

    print("=== Peeking ๋ฌธ์ œ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ===")
    print(f"์‹œ๋ฎฌ๋ ˆ์ด์…˜ ํšŸ์ˆ˜: {n_simulations:,}")
    print(f"์ด ํ‘œ๋ณธ ํฌ๊ธฐ: {n_total}")
    print(f"์ค‘๊ฐ„ ํ™•์ธ ํšŸ์ˆ˜: {n_looks}")
    print(f"๋ชฉํ‘œ ฮฑ: {alpha}")
    print(f"\n๊ณ ์ • ํ‘œ๋ณธ ๊ฒ€์ • ์œ„์–‘์„ฑ๋ฅ : {fpr_fixed:.4f} ({fpr_fixed*100:.2f}%)")
    print(f"Peeking ๊ฒ€์ • ์œ„์–‘์„ฑ๋ฅ : {fpr_peeking:.4f} ({fpr_peeking*100:.2f}%)")
    print(f"์œ„์–‘์„ฑ๋ฅ  ์ฆ๊ฐ€: {(fpr_peeking/alpha - 1)*100:.1f}%")

    return fpr_fixed, fpr_peeking

fpr_fixed, fpr_peeking = simulate_peeking_problem()

4.3 Alpha Spending ํ•จ์ˆ˜

def alpha_spending_pocock(t, alpha=0.05):
    """Pocock alpha spending function"""
    return alpha * np.log(1 + (np.e - 1) * t)

def alpha_spending_obrien_fleming(t, alpha=0.05):
    """O'Brien-Fleming alpha spending function"""
    return 2 * (1 - norm.cdf(norm.ppf(1 - alpha/2) / np.sqrt(t)))

def plot_alpha_spending():
    """Alpha spending ํ•จ์ˆ˜ ์‹œ๊ฐํ™”"""
    t = np.linspace(0.01, 1, 100)
    alpha = 0.05

    fig, ax = plt.subplots(figsize=(10, 6))

    ax.plot(t, alpha_spending_pocock(t, alpha), label='Pocock', linewidth=2)
    ax.plot(t, alpha_spending_obrien_fleming(t, alpha), label="O'Brien-Fleming", linewidth=2)
    ax.plot(t, t * alpha, '--', label='Linear (reference)', alpha=0.5)
    ax.axhline(alpha, color='r', linestyle=':', label=f'Total ฮฑ={alpha}')

    ax.set_xlabel('์ •๋ณด ๋น„์œจ (ํ˜„์žฌ/์ตœ์ข…)')
    ax.set_ylabel('๋ˆ„์  ฮฑ spent')
    ax.set_title('Alpha Spending Functions')
    ax.legend()
    ax.grid(True, alpha=0.3)
    ax.set_xlim(0, 1)
    ax.set_ylim(0, alpha * 1.1)

    plt.show()

    print("=== Alpha Spending ํ•จ์ˆ˜ ๋น„๊ต ===")
    print("\nPocock:")
    print("  - ๊ฐ ๋ถ„์„์—์„œ ๋™์ผํ•œ ์ž„๊ณ„๊ฐ’")
    print("  - ์กฐ๊ธฐ ์ข…๋ฃŒ์— ๋” ๊ด€๋Œ€")
    print("  - ์ตœ์ข… ๋ถ„์„์—์„œ ๋” ๋ณด์ˆ˜์ ")

    print("\nO'Brien-Fleming:")
    print("  - ์ดˆ๊ธฐ์— ๋งค์šฐ ๋ณด์ˆ˜์  (๋†’์€ ์ž„๊ณ„๊ฐ’)")
    print("  - ํ›„๊ธฐ์— ๊ณ ์ • ํ‘œ๋ณธ๊ณผ ์œ ์‚ฌ")
    print("  - ์กฐ๊ธฐ ์ข…๋ฃŒ๋Š” ๊ทน๋‹จ์  ํšจ๊ณผ์—์„œ๋งŒ")

plot_alpha_spending()

4.4 ์ˆœ์ฐจ์  ๊ฒ€์ • ๊ตฌํ˜„

class SequentialTest:
    """์ˆœ์ฐจ์  A/B ํ…Œ์ŠคํŠธ"""

    def __init__(self, max_n, n_looks, alpha=0.05, spending='obrien_fleming'):
        """
        Parameters:
        -----------
        max_n : int
            ์ตœ๋Œ€ ํ‘œ๋ณธ ํฌ๊ธฐ (๊ฐ ๊ทธ๋ฃน)
        n_looks : int
            ์ค‘๊ฐ„ ๋ถ„์„ ํšŸ์ˆ˜
        alpha : float
            ์ „์ฒด ์œ ์˜์ˆ˜์ค€
        spending : str
            'pocock' or 'obrien_fleming'
        """
        self.max_n = max_n
        self.n_looks = n_looks
        self.alpha = alpha
        self.spending = spending

        # ๋ถ„์„ ์‹œ์ 
        self.look_times = np.linspace(1/n_looks, 1, n_looks)

        # ๊ฐ ๋ถ„์„์—์„œ ์‚ฌ์šฉํ•  alpha
        self.alphas = self._compute_alphas()

    def _compute_alphas(self):
        """๊ฐ ๋ถ„์„ ์‹œ์ ์˜ alpha ๊ณ„์‚ฐ"""
        if self.spending == 'pocock':
            cumulative = [alpha_spending_pocock(t, self.alpha) for t in self.look_times]
        else:
            cumulative = [alpha_spending_obrien_fleming(t, self.alpha) for t in self.look_times]

        # ์ฆ๋ถ„ alpha
        alphas = [cumulative[0]]
        for i in range(1, len(cumulative)):
            alphas.append(cumulative[i] - cumulative[i-1])

        return alphas

    def critical_values(self):
        """๊ฐ ๋ถ„์„์˜ ์ž„๊ณ„ Z ๊ฐ’"""
        return [norm.ppf(1 - a/2) for a in self.alphas]

    def summary(self):
        """๋ถ„์„ ๊ณ„ํš ์š”์•ฝ"""
        print("=== ์ˆœ์ฐจ์  ๊ฒ€์ • ๊ณ„ํš ===")
        print(f"์ตœ๋Œ€ ํ‘œ๋ณธ: {self.max_n} (๊ฐ ๊ทธ๋ฃน)")
        print(f"์ค‘๊ฐ„ ๋ถ„์„: {self.n_looks}ํšŒ")
        print(f"์ „์ฒด ฮฑ: {self.alpha}")
        print(f"Spending: {self.spending}")

        print("\n๋ถ„์„ ์‹œ์ ๋ณ„ ๊ณ„ํš:")
        print("-" * 50)
        print(f"{'๋ถ„์„':<6} {'n':<10} {'๋ˆ„์  ฮฑ':<12} {'์ฆ๋ถ„ ฮฑ':<12} {'Z ์ž„๊ณ„๊ฐ’':<10}")
        print("-" * 50)

        cumulative_alpha = 0
        z_crits = self.critical_values()

        for i, (t, a) in enumerate(zip(self.look_times, self.alphas)):
            n = int(t * self.max_n)
            cumulative_alpha += a
            print(f"{i+1:<6} {n:<10} {cumulative_alpha:<12.4f} {a:<12.4f} {z_crits[i]:<10.3f}")


# ์˜ˆ์‹œ
seq_test = SequentialTest(max_n=5000, n_looks=5, alpha=0.05, spending='obrien_fleming')
seq_test.summary()

print("\n")

seq_test_pocock = SequentialTest(max_n=5000, n_looks=5, alpha=0.05, spending='pocock')
seq_test_pocock.summary()

5. ์ผ๋ฐ˜์ ์ธ ํ•จ์ •๊ณผ ์ฃผ์˜์‚ฌํ•ญ

5.1 ๋‹ค์ค‘ ๋น„๊ต ๋ฌธ์ œ

def multiple_comparisons_problem():
    """๋‹ค์ค‘ ๋น„๊ต ๋ฌธ์ œ"""
    print("""
    =================================================
    ๋‹ค์ค‘ ๋น„๊ต ๋ฌธ์ œ (Multiple Comparisons)
    =================================================

    ๋ฌธ์ œ:
    - ์—ฌ๋Ÿฌ ๊ฒ€์ •์„ ๋™์‹œ์— ์ˆ˜ํ–‰ํ•˜๋ฉด ์ œ1์ข… ์˜ค๋ฅ˜ ์ฆ๊ฐ€
    - k๊ฐœ ๊ฒ€์ • ์‹œ ์ตœ์†Œ ํ•˜๋‚˜ ์œ„์–‘์„ฑ ํ™•๋ฅ : 1 - (1-ฮฑ)^k

    ์˜ˆ์‹œ (ฮฑ=0.05):
    - 1๊ฐœ ๊ฒ€์ •: 5%
    - 5๊ฐœ ๊ฒ€์ •: 23%
    - 10๊ฐœ ๊ฒ€์ •: 40%
    - 20๊ฐœ ๊ฒ€์ •: 64%

    ๋ณด์ • ๋ฐฉ๋ฒ•:
    โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    1. Bonferroni: ฮฑ' = ฮฑ/k (๊ฐ€์žฅ ๋ณด์ˆ˜์ )
    2. Holm-Bonferroni: ์ˆœ์ฐจ์  Bonferroni
    3. Benjamini-Hochberg (FDR): ์œ„๋ฐœ๊ฒฌ๋ฅ  ํ†ต์ œ
    4. ์‚ฌ์ „ ๋“ฑ๋ก: ์ฃผ์š” ๊ฐ€์„ค 1๊ฐœ ์ง€์ •
    """)

    # ์‹œ๊ฐํ™”
    k_values = range(1, 21)
    alpha = 0.05

    fwer = [1 - (1 - alpha)**k for k in k_values]
    bonferroni = [min(alpha * k, 1.0) for k in k_values]  # ๋ณด์ • ์ „ ํ—ˆ์šฉ ๋ฒ”์œ„

    fig, ax = plt.subplots(figsize=(10, 6))
    ax.plot(k_values, fwer, 'b-o', label='๋ณด์ • ์—†์Œ (FWER)')
    ax.axhline(alpha, color='r', linestyle='--', label=f'๋ชฉํ‘œ ฮฑ={alpha}')
    ax.set_xlabel('๊ฒ€์ • ํšŸ์ˆ˜')
    ax.set_ylabel('์ตœ์†Œ 1๊ฐœ ์œ„์–‘์„ฑ ํ™•๋ฅ ')
    ax.set_title('๋‹ค์ค‘ ๋น„๊ต ๋ฌธ์ œ: ๊ฒ€์ • ํšŸ์ˆ˜์™€ ์œ„์–‘์„ฑ๋ฅ ')
    ax.legend()
    ax.grid(True, alpha=0.3)
    ax.set_ylim(0, 0.7)

    plt.show()

multiple_comparisons_problem()

5.2 ๊ธฐํƒ€ ์ฃผ์˜์‚ฌํ•ญ

def common_pitfalls():
    """A/B ํ…Œ์ŠคํŠธ์˜ ์ผ๋ฐ˜์ ์ธ ํ•จ์ •"""
    print("""
    =================================================
    A/B ํ…Œ์ŠคํŠธ ์ฃผ์˜์‚ฌํ•ญ
    =================================================

    1. Peeking (์ค‘๊ฐ„ ํ™•์ธ)
       - ๋ฌธ์ œ: ์›ํ•˜๋Š” ๊ฒฐ๊ณผ๊ฐ€ ๋‚˜์˜ฌ ๋•Œ๊นŒ์ง€ ํ™•์ธ
       - ํ•ด๊ฒฐ: ์ˆœ์ฐจ์  ๊ฒ€์ • ๋˜๋Š” ๊ณ ์ • ํ‘œ๋ณธ

    2. ๋‹ค์ค‘ ๋น„๊ต
       - ๋ฌธ์ œ: ์—ฌ๋Ÿฌ ๋ฉ”ํŠธ๋ฆญ/์„ธ๊ทธ๋จผํŠธ ํ…Œ์ŠคํŠธ
       - ํ•ด๊ฒฐ: ์‚ฌ์ „ ๋“ฑ๋ก, ๋ณด์ •, ์ฃผ์š” ๋ฉ”ํŠธ๋ฆญ ์ง€์ •

    3. ๋ถ€์ ์ ˆํ•œ ํ‘œ๋ณธ ํฌ๊ธฐ
       - ๋ฌธ์ œ: ๋„ˆ๋ฌด ์ž‘์œผ๋ฉด ํšจ๊ณผ ํƒ์ง€ ์‹คํŒจ, ๋„ˆ๋ฌด ํฌ๋ฉด ๋‚ญ๋น„
       - ํ•ด๊ฒฐ: ์‚ฌ์ „ ๊ฒ€์ •๋ ฅ ๋ถ„์„

    4. ์‹ ๊ทœ ํšจ๊ณผ (Novelty Effect)
       - ๋ฌธ์ œ: ์ƒˆ๋กœ์›€ ์ž์ฒด๊ฐ€ ์ผ์‹œ์  ํšจ๊ณผ ์œ ๋ฐœ
       - ํ•ด๊ฒฐ: ์ถฉ๋ถ„ํ•œ ์‹คํ—˜ ๊ธฐ๊ฐ„

    5. ๋„คํŠธ์›Œํฌ ํšจ๊ณผ (Spillover)
       - ๋ฌธ์ œ: ๊ทธ๋ฃน ๊ฐ„ ์ƒํ˜ธ์ž‘์šฉ
       - ํ•ด๊ฒฐ: ํด๋Ÿฌ์Šคํ„ฐ ๋ฌด์ž‘์œ„ํ™”

    6. Simpson's Paradox
       - ๋ฌธ์ œ: ์ „์ฒด vs ์„ธ๊ทธ๋จผํŠธ๋ณ„ ๊ฒฐ๊ณผ ์ƒ์ถฉ
       - ํ•ด๊ฒฐ: ์ธตํ™” ๋ถ„์„, ์ธ๊ณผ ๊ทธ๋ž˜ํ”„

    7. ์‹ค์ œ์  ์œ ์˜์„ฑ ๋ฌด์‹œ
       - ๋ฌธ์ œ: ํ†ต๊ณ„์  ์œ ์˜์„ฑ โ‰  ์‹ค์ œ์  ์ค‘์š”์„ฑ
       - ํ•ด๊ฒฐ: ํšจ๊ณผ ํฌ๊ธฐ, ์‹ ๋ขฐ๊ตฌ๊ฐ„, ๋น„์ฆˆ๋‹ˆ์Šค ์˜ํ–ฅ ๊ณ ๋ ค

    8. ๊ฒ€์ •๋ ฅ ๋ถ€์กฑ
       - ๋ฌธ์ œ: "ํšจ๊ณผ ์—†์Œ" โ‰  "๊ท€๋ฌด๊ฐ€์„ค ์ฐธ"
       - ํ•ด๊ฒฐ: ๊ฒ€์ •๋ ฅ ๋ณด๊ณ , ๋™๋“ฑ์„ฑ ๊ฒ€์ •
    """)

common_pitfalls()

6. ์‹ค์Šต ์˜ˆ์ œ

6.1 ์ข…ํ•ฉ ์‹คํ—˜ ์„ค๊ณ„

def complete_ab_test_workflow():
    """A/B ํ…Œ์ŠคํŠธ ์ „์ฒด ์›Œํฌํ”Œ๋กœ์šฐ"""

    print("="*60)
    print("A/B ํ…Œ์ŠคํŠธ ์›Œํฌํ”Œ๋กœ์šฐ")
    print("="*60)

    # 1. ๊ฐ€์„ค ์ˆ˜๋ฆฝ
    print("\n[1๋‹จ๊ณ„] ๊ฐ€์„ค ์ˆ˜๋ฆฝ")
    print("  H0: ์ƒˆ ๋ฒ„ํŠผ ์ƒ‰์ƒ์€ ์ „ํ™˜์œจ์— ์˜ํ–ฅ ์—†์Œ")
    print("  H1: ์ƒˆ ๋ฒ„ํŠผ ์ƒ‰์ƒ์€ ์ „ํ™˜์œจ์„ ๋ณ€ํ™”์‹œํ‚ด")

    # 2. ๋ฉ”ํŠธ๋ฆญ ์ •์˜
    print("\n[2๋‹จ๊ณ„] ๋ฉ”ํŠธ๋ฆญ ์ •์˜")
    baseline_rate = 0.05  # 5%
    mde = 0.01  # ์ตœ์†Œ ํƒ์ง€ ํšจ๊ณผ: 1%p
    print(f"  ๊ธฐ์ค€ ์ „ํ™˜์œจ: {baseline_rate:.1%}")
    print(f"  MDE (Minimum Detectable Effect): {mde:.1%}p")

    # 3. ํ‘œ๋ณธ ํฌ๊ธฐ ๊ณ„์‚ฐ
    print("\n[3๋‹จ๊ณ„] ํ‘œ๋ณธ ํฌ๊ธฐ ๊ณ„์‚ฐ")
    target_rate = baseline_rate + mde
    n1, n2 = sample_size_two_proportions(baseline_rate, target_rate, alpha=0.05, power=0.80)
    print(f"  ํ•„์š” ํ‘œ๋ณธ ํฌ๊ธฐ: {n1:,} (๊ฐ ๊ทธ๋ฃน)")
    print(f"  ์ด ํ•„์š” ํŠธ๋ž˜ํ”ฝ: {n1 + n2:,}")

    # 4. ์‹คํ—˜ ์‹คํ–‰ (์‹œ๋ฎฌ๋ ˆ์ด์…˜)
    print("\n[4๋‹จ๊ณ„] ์‹คํ—˜ ์‹คํ–‰ (์‹œ๋ฎฌ๋ ˆ์ด์…˜)")
    np.random.seed(42)
    n_control = n1
    n_treatment = n2
    x_control = np.random.binomial(n_control, baseline_rate)
    x_treatment = np.random.binomial(n_treatment, baseline_rate + mde * 0.8)  # ์‹ค์ œ ํšจ๊ณผ๋Š” MDE์˜ 80%

    print(f"  Control: {x_control:,}/{n_control:,} = {x_control/n_control:.2%}")
    print(f"  Treatment: {x_treatment:,}/{n_treatment:,} = {x_treatment/n_treatment:.2%}")

    # 5. ๋ถ„์„
    print("\n[5๋‹จ๊ณ„] ๋ถ„์„")
    ab = ABTest(n_control, x_control, n_treatment, x_treatment)
    z, p = ab.z_test()
    diff, ci = ab.confidence_interval()
    lift = ab.lift()

    print(f"  ์ฐจ์ด: {diff:.4f} ({diff*100:.2f}%p)")
    print(f"  ์ƒ์Šน๋ฅ : {lift*100:.2f}%")
    print(f"  95% CI: ({ci[0]*100:.2f}%p, {ci[1]*100:.2f}%p)")
    print(f"  Z ํ†ต๊ณ„๋Ÿ‰: {z:.3f}")
    print(f"  p-value: {p:.4f}")

    # 6. ์˜์‚ฌ๊ฒฐ์ •
    print("\n[6๋‹จ๊ณ„] ์˜์‚ฌ๊ฒฐ์ •")
    if p < 0.05:
        if diff > 0:
            print("  ๊ฒฐ๋ก : Treatment ์ฑ„ํƒ (ํ†ต๊ณ„์ ์œผ๋กœ ์œ ์˜ํ•œ ๊ฐœ์„ )")
        else:
            print("  ๊ฒฐ๋ก : Control ์œ ์ง€ (Treatment๊ฐ€ ๋” ๋‚˜์จ)")
    else:
        print("  ๊ฒฐ๋ก : ๊ฒฐ์ • ๋ณด๋ฅ˜ (์œ ์˜ํ•œ ์ฐจ์ด ์—†์Œ)")
        print("  ๊ณ ๋ ค์‚ฌํ•ญ: ํ‘œ๋ณธ ํฌ๊ธฐ ์ฆ๊ฐ€ ๋˜๋Š” ๋‹ค๋ฅธ ๋ณ€ํ˜• ํ…Œ์ŠคํŠธ")

    return ab

ab_result = complete_ab_test_workflow()

7. ์—ฐ์Šต ๋ฌธ์ œ

๋ฌธ์ œ 1: ํ‘œ๋ณธ ํฌ๊ธฐ ๊ณ„์‚ฐ

๊ธฐ์กด ์ „ํ™˜์œจ์ด 3%์ด๊ณ , ์ตœ์†Œ 20%์˜ ์ƒ๋Œ€์  ์ƒ์Šน(3.6%๋กœ)์„ ํƒ์ง€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด: 1. ฮฑ=0.05, Power=0.80์—์„œ ํ•„์š”ํ•œ ํ‘œ๋ณธ ํฌ๊ธฐ 2. Power=0.90์œผ๋กœ ๋†’์ด๋ฉด ํ‘œ๋ณธ ํฌ๊ธฐ ๋ณ€ํ™” 3. MDE๋ฅผ 10% ์ƒ์Šน์œผ๋กœ ๋‚ฎ์ถ”๋ฉด ํ‘œ๋ณธ ํฌ๊ธฐ ๋ณ€ํ™”

๋ฌธ์ œ 2: ์‹คํ—˜ ๊ธฐ๊ฐ„ ์ถ”์ •

์ผ์ผ ํŠธ๋ž˜ํ”ฝ์ด 10,000 ๋ฐฉ๋ฌธ์ด๊ณ , 50:50์œผ๋กœ ๋ถ„ํ• ํ•œ๋‹ค๋ฉด: 1. ๋ฌธ์ œ 1์˜ ํ‘œ๋ณธ ํฌ๊ธฐ๋ฅผ ํ™•๋ณดํ•˜๋Š”๋ฐ ํ•„์š”ํ•œ ๊ธฐ๊ฐ„ 2. ์ฃผ๋ง ํšจ๊ณผ๋ฅผ ๊ณ ๋ คํ•˜๋ฉด ์ตœ์†Œ ๋ช‡ ์ฃผ ์‹คํ—˜?

๋ฌธ์ œ 3: ์ˆœ์ฐจ์  ๊ฒ€์ • ์„ค๊ณ„

5ํšŒ ์ค‘๊ฐ„ ๋ถ„์„์„ ๊ณ„ํšํ•œ๋‹ค๋ฉด: 1. O'Brien-Fleming ๋ฐฉ๋ฒ•์˜ ๊ฐ ๋ถ„์„ ์ž„๊ณ„๊ฐ’ 2. Pocock ๋ฐฉ๋ฒ•๊ณผ ๋น„๊ต 3. ์ฒซ ๋ฒˆ์งธ ๋ถ„์„์—์„œ ์กฐ๊ธฐ ์ข…๋ฃŒ ์กฐ๊ฑด

๋ฌธ์ œ 4: ๋‹ค์ค‘ ๋น„๊ต ๋ณด์ •

5๊ฐœ ์„ธ๊ทธ๋จผํŠธ(์—ฐ๋ น๋Œ€)์—์„œ A/B ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๋ฅผ ๋ถ„์„ํ•œ๋‹ค๋ฉด: 1. Bonferroni ๋ณด์ •๋œ ์œ ์˜์ˆ˜์ค€ 2. ํ•˜๋‚˜์˜ ์„ธ๊ทธ๋จผํŠธ์—์„œ p=0.02๊ฐ€ ๋‚˜์™”์„ ๋•Œ ๊ฒฐ๋ก  3. ์‚ฌ์ „ ๋“ฑ๋กํ–ˆ๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ๋‹ค๋ฅผ๊นŒ?


8. ํ•ต์‹ฌ ์š”์•ฝ

์‹คํ—˜ ์„ค๊ณ„ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

  1. [ ] ๋ช…ํ™•ํ•œ ๊ฐ€์„ค๊ณผ ๋ฉ”ํŠธ๋ฆญ ์ •์˜
  2. [ ] ๊ฒ€์ •๋ ฅ ๋ถ„์„์œผ๋กœ ํ‘œ๋ณธ ํฌ๊ธฐ ๊ฒฐ์ •
  3. [ ] ๋ฌด์ž‘์œ„ํ™” ๋ฐฉ๋ฒ• ์„ ํƒ
  4. [ ] ์‹คํ—˜ ๊ธฐ๊ฐ„ ์„ค์ • (์ฃผ๊ฐ„ ํšจ๊ณผ ๊ณ ๋ ค)
  5. [ ] ์ค‘๊ฐ„ ๋ถ„์„ ๊ณ„ํš (์ˆœ์ฐจ์  ๊ฒ€์ •)
  6. [ ] ๋‹ค์ค‘ ๋น„๊ต ๊ณ ๋ ค
  7. [ ] ์‚ฌ์ „ ๋“ฑ๋ก

ํ‘œ๋ณธ ํฌ๊ธฐ ๊ณต์‹ (๋น„์œจ ๋น„๊ต)

$$n = \frac{(z_{\alpha/2}\sqrt{2\bar{p}(1-\bar{p})} + z_{\beta}\sqrt{p_1(1-p_1)+p_2(1-p_2)})^2}{(p_1-p_2)^2}$$

๊ฒ€์ •๋ ฅ ๊ด€๊ณ„

์š”์ธ ์ฆ๊ฐ€ ์‹œ ํ‘œ๋ณธ ํฌ๊ธฐ
ํšจ๊ณผ ํฌ๊ธฐ โ†‘ ๊ฐ์†Œ
๊ฒ€์ •๋ ฅ โ†‘ ์ฆ๊ฐ€
ฮฑ โ†“ (๋” ์—„๊ฒฉ) ์ฆ๊ฐ€
๋ถ„์‚ฐ โ†‘ ์ฆ๊ฐ€

์ˆœ์ฐจ์  ๊ฒ€์ •

๋ฐฉ๋ฒ• ์ดˆ๊ธฐ ํ›„๊ธฐ
O'Brien-Fleming ๋งค์šฐ ๋ณด์ˆ˜์  ๊ณ ์ • ํ‘œ๋ณธ ์œ ์‚ฌ
Pocock ์ผ์ • ๋” ๋ณด์ˆ˜์ 

Python ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

from statsmodels.stats.power import TTestPower, NormalIndPower
from statsmodels.stats.proportion import proportion_effectsize
from scipy.stats import norm

# ํ‘œ๋ณธ ํฌ๊ธฐ ๊ณ„์‚ฐ
power_analysis = NormalIndPower()
n = power_analysis.solve_power(effect_size=h, alpha=0.05, power=0.80)
to navigate between lessons