Agent 실험의 표본 크기와 검정력

적은 트래픽에서 의미 있는 실험을 설계하는 방법

기업 내부 Agent는 일일 트래픽이 수십~수백 건에 불과하다. 이 환경에서 MDE(최소감지효과)를 현실적으로 설정하고, 검정력을 확보하기 위한 전략을 다룬다. 분산 감소, 층화, 복합 메트릭 등 표본 크기를 줄이는 실무적 기법을 제시한다.

Experimentation
Statistics
저자

Kwangmin Kim

공개

2026년 03월 21일

1 정의

정의: 검정력 분석 (Power Analysis)

검정력 분석이란, 실험에서 실제로 존재하는 효과를 감지할 확률(검정력, \(1-\beta\))을 사전에 계산하고, 이를 기반으로 필요한 표본 크기를 결정하는 과정이다.

네 가지 요소가 상호 연결되어 있으며, 셋을 고정하면 나머지 하나가 결정된다:

  • 유의 수준 (\(\alpha\)): 1종 오류 허용 확률 (보통 0.05)

  • 검정력 (\(1-\beta\)): 2종 오류를 피할 확률 (보통 0.80)

  • 효과 크기 (MDE): 감지하고자 하는 최소 차이

  • 표본 크기 (\(n\)): 그룹당 필요한 관측 수

  • 역학: Power analysis, Sample size determination (임상시험 설계의 핵심)

  • IT: Power analysis, Traffic estimation


2 Agent 실험에서 표본 크기가 중요한 이유

2.1 적은 트래픽의 현실

환경 일일 질의 수 50:50 분할 시 그룹당/일
Google 검색 수십억 수십억
중형 SaaS 수만 수만
MINERVA QnA Chatbot ~50 ~25
MINERVA Data Std Helper ~30 ~15
MINERVA Insilico Agent ~20 ~10

Google은 하루 만에 수백만 표본을 모은다. MINERVA는 한 달이 걸려도 750건(QnA 기준)이다. 이 차이가 실험 설계의 모든 결정을 바꾼다.

2.2 검정력 부족의 결과

표본이 부족하면:

  • 실제로 효과가 있어도 감지하지 못한다 (2종 오류, false negative)
  • “유의하지 않다”는 결론이 “효과가 없다”가 아니라 “모르겠다”가 된다
  • 의사결정을 내릴 수 없어 실험이 낭비된다

3 MDE 설정 가이드

3.1 MDE란

Minimum Detectable Effect(최소감지효과)는 “이 정도 차이는 감지하고 싶다”는 기준이다. MDE를 작게 설정할수록 더 많은 표본이 필요하다.

\[ n = \frac{(z_{\alpha/2} + z_\beta)^2 \cdot 2\sigma^2}{\text{MDE}^2} \]

3.2 Agent 메트릭별 MDE 가이드라인

메트릭 스케일 현재 기준선 (예시) 권장 MDE 근거
Relevance Score 1-5 3.5 (σ≈1.0) 0.3~0.5 5점 척도에서 0.3점은 실무적으로 의미 있는 차이
Faithfulness Score 1-5 4.0 (σ≈0.8) 0.3~0.5 환각 감소는 작은 차이도 중요
Hit Rate@5 0-1 0.70 0.05~0.10 70% → 75%는 20건 중 1건 추가 hit
Task Completion 0-1 0.60 0.05~0.10 비즈니스 임팩트 기준
Latency (ms) 연속 2000 (σ≈500) 200~500 사용자 체감 차이

3.3 표본 크기 계산

import numpy as np
from scipy.stats import norm

def sample_size_continuous(mde, sigma, alpha=0.05, power=0.80):
    """연속형 메트릭의 그룹당 필요 표본 크기

    Args:
        mde: 감지하고자 하는 최소 차이 (원래 스케일)
        sigma: 메트릭의 표준편차
        alpha: 유의수준
        power: 검정력
    """
    z_alpha = norm.ppf(1 - alpha / 2)
    z_beta = norm.ppf(power)
    n = 2 * ((z_alpha + z_beta) * sigma / mde) ** 2
    return int(np.ceil(n))

def sample_size_proportion(p_control, mde, alpha=0.05, power=0.80):
    """이진형 메트릭의 그룹당 필요 표본 크기

    Args:
        p_control: 대조군의 기대 비율
        mde: 감지하고자 하는 절대 차이 (예: 0.05 = 5%p)
    """
    p_treat = p_control + mde
    p_avg = (p_control + p_treat) / 2
    sigma = np.sqrt(2 * p_avg * (1 - p_avg))
    z_alpha = norm.ppf(1 - alpha / 2)
    z_beta = norm.ppf(power)
    n = ((z_alpha + z_beta) / mde) ** 2 * 2 * p_avg * (1 - p_avg)
    return int(np.ceil(n))

# MINERVA QnA Chatbot 예시
scenarios = {
    "Relevance (MDE=0.3, σ=1.0)": sample_size_continuous(0.3, 1.0),
    "Relevance (MDE=0.5, σ=1.0)": sample_size_continuous(0.5, 1.0),
    "Hit Rate (p=0.70, MDE=0.05)": sample_size_proportion(0.70, 0.05),
    "Hit Rate (p=0.70, MDE=0.10)": sample_size_proportion(0.70, 0.10),
}

for scenario, n in scenarios.items():
    print(f"{scenario}: 그룹당 {n}건, 총 {2*n}건")

# 출력 예시:
# Relevance (MDE=0.3, σ=1.0): 그룹당 176건, 총 352건
# Relevance (MDE=0.5, σ=1.0): 그룹당 64건, 총 128건
# Hit Rate (p=0.70, MDE=0.05): 그룹당 1231건, 총 2462건
# Hit Rate (p=0.70, MDE=0.10): 그룹당 313건, 총 626건

3.4 실험 기간 환산

def experiment_duration(n_per_group, daily_queries, split_ratio=0.5):
    """필요 표본 크기에서 실험 기간을 계산한다"""
    queries_per_group_per_day = daily_queries * split_ratio
    days = np.ceil(n_per_group / queries_per_group_per_day)
    return {
        "n_per_group": n_per_group,
        "daily_queries": daily_queries,
        "days": int(days),
        "weeks": round(days / 7, 1),
    }

# MINERVA 시나리오
print(experiment_duration(176, daily_queries=50))
# → {'n_per_group': 176, 'daily_queries': 50, 'days': 8, 'weeks': 1.1}

print(experiment_duration(176, daily_queries=20))
# → {'n_per_group': 176, 'daily_queries': 20, 'days': 18, 'weeks': 2.6}

print(experiment_duration(1231, daily_queries=50))
# → {'n_per_group': 1231, 'daily_queries': 50, 'days': 50, 'weeks': 7.1}

4 표본 크기를 줄이는 전략

4.1 MDE를 현실적으로 조정

가장 직접적인 방법이다. “작은 효과도 놓치고 싶지 않다”는 욕심을 버리고, 의사결정에 실제로 영향을 주는 크기만 감지하도록 설정한다.

메트릭 작은 MDE (비현실적) 현실적 MDE 표본 감소율
Relevance 0.1 (σ=1.0) 0.5 ~96% 감소
Hit Rate 0.02 0.10 ~96% 감소

4.2 분산 감소 (Variance Reduction)

메트릭의 분산(\(\sigma^2\))을 줄이면 동일 MDE에서 필요 표본이 줄어든다.

4.2.1 층화 (Stratification)

질의 유형별로 층화하면 그룹 간 불균형을 줄이고 분산을 감소시킨다.

def stratified_assignment(query, user_id, experiment_id, strata_map):
    """질의 유형별 층화 배정

    각 층(stratum) 내에서 50:50 배정을 보장한다.
    """
    # 질의 유형 분류
    query_type = classify_query_type(query)  # "정의", "비교", "절차" 등
    stratum = strata_map.get(query_type, "default")

    # 층 내에서 결정적 해싱
    hash_input = f"{experiment_id}:{stratum}:{user_id}"
    hash_value = int(hashlib.sha256(hash_input.encode()).hexdigest(), 16)
    return "control" if (hash_value % 10000) < 5000 else "treatment"

4.2.2 CUPED (Controlled-experiment Using Pre-Experiment Data)

실험 전 데이터(과거 응답 품질)를 공변량으로 사용하여 분산을 줄인다.

\[ \hat{Y}^{cuped} = Y - \theta(X - \bar{X}) \]

여기서 \(X\)는 실험 전 메트릭, \(\theta = \text{Cov}(X, Y) / \text{Var}(X)\)이다.

직관적으로, CUPED는 시험 점수에서 “원래 실력 차이”를 빼는 것이다. 학생 A는 원래 수학을 잘하고 학생 B는 못한다면, 새 교수법 실험에서 A의 점수가 높은 것은 교수법 효과가 아니라 원래 실력이다. CUPED는 실험 전 점수(\(X\))로 이 “원래 실력” 부분을 제거하여, 순수한 처치 효과만 남긴다. Agent 실험에서는 “이 질의 유형은 원래 어렵다”는 변동을 제거하는 것과 같다 — 쉬운 질의는 어떤 구성이든 점수가 높고, 어려운 질의는 낮으므로, 이 패턴을 빼면 구성 간 차이가 더 선명하게 드러난다.

def cuped_adjustment(y_experiment, x_pre_experiment):
    """CUPED 보정으로 분산을 줄인다

    Args:
        y_experiment: 실험 기간 메트릭
        x_pre_experiment: 실험 전 동일 메트릭 (또는 상관 높은 메트릭)
    """
    theta = np.cov(y_experiment, x_pre_experiment)[0, 1] / np.var(x_pre_experiment)
    y_adjusted = y_experiment - theta * (x_pre_experiment - np.mean(x_pre_experiment))

    variance_reduction = 1 - np.var(y_adjusted) / np.var(y_experiment)
    return {
        "y_adjusted": y_adjusted,
        "theta": theta,
        "variance_reduction": f"{variance_reduction:.1%}",
        "original_std": np.std(y_experiment),
        "adjusted_std": np.std(y_adjusted),
    }
Agent에서 CUPED 적용

Agent 실험에서 “실험 전 데이터”란?

  • 실험 이전 기간에 동일 질의 유형에 대한 평균 Relevance Score
  • 질의의 난이도 점수 (Golden Dataset에서 산출)
  • 과거 동일 사용자의 평균 만족도

이 공변량과 실험 메트릭 간 상관이 높을수록 분산 감소 효과가 크다 (상관 0.5면 약 25% 분산 감소).

4.3 비열등성 설계 (Non-inferiority)

“새 구성이 더 좋은가?”가 아니라 “새 구성이 기존보다 나쁘지 않은가?”를 검정한다. 비용이 절감되거나 유지보수가 쉬운 구성이 기존과 동등하다면 채택할 가치가 있다.

비열등성 마진 \(\delta\)를 설정하고, \(H_0: \mu_T - \mu_C \leq -\delta\)를 검정한다.

이 설계는 우월성 검정보다 적은 표본으로 결론을 내릴 수 있다.

4.4 Paired Design

동일 질의에 대해 두 구성을 모두 실행하고 점수 차이를 검정한다 (paired t-test). 질의 간 변동이 제거되므로 분산이 크게 줄어든다.

def paired_sample_size(mde, sigma_diff, alpha=0.05, power=0.80):
    """대응 표본 설계의 필요 표본 크기

    Args:
        sigma_diff: 점수 차이(d = y_A - y_B)의 표준편차
    """
    z_alpha = norm.ppf(1 - alpha / 2)
    z_beta = norm.ppf(power)
    n = ((z_alpha + z_beta) * sigma_diff / mde) ** 2
    return int(np.ceil(n))

# 독립 설계: σ=1.0, MDE=0.3 → n=176/그룹
# 대응 설계: σ_diff=0.7 (질의 간 변동 제거), MDE=0.3 → n=86
n_paired = paired_sample_size(0.3, 0.7)
print(f"대응 설계: {n_paired}쌍 필요 (독립 설계 대비 {86/176:.0%})")
Paired Design의 제약

오프라인 평가에서는 자연스럽지만, 온라인 실험에서는 같은 사용자에게 같은 질의로 두 번 응답하는 것이 불가능하다. 온라인에서 paired design을 적용하려면 interleaving 등의 특수 기법이 필요하다.


5 검정력 곡선 시각화

import matplotlib.pyplot as plt
import numpy as np
from scipy.stats import norm

def power_curve(n_per_group, sigma=1.0, alpha=0.05):
    """주어진 표본 크기에서 MDE별 검정력을 계산한다"""
    mde_range = np.linspace(0.05, 1.0, 100)
    z_alpha = norm.ppf(1 - alpha / 2)
    powers = []
    for mde in mde_range:
        se = sigma * np.sqrt(2 / n_per_group)
        z_beta = mde / se - z_alpha
        powers.append(norm.cdf(z_beta))
    return mde_range, np.array(powers)

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

# 왼쪽: 표본 크기별 검정력 곡선
for n in [50, 100, 200, 500]:
    mdes, powers = power_curve(n)
    axes[0].plot(mdes, powers, label=f"n={n}/group")
axes[0].axhline(y=0.8, color="red", linestyle="--", alpha=0.5, label="Power=0.80")
axes[0].set_xlabel("MDE (Relevance Score 차이)")
axes[0].set_ylabel("검정력 (Power)")
axes[0].set_title("표본 크기별 검정력 곡선")
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# 오른쪽: MINERVA 트래픽별 실험 기간
daily_queries = [20, 50, 100]
mde_range = np.linspace(0.1, 1.0, 50)
for dq in daily_queries:
    days = []
    for mde in mde_range:
        n = 2 * ((1.96 + 0.84) * 1.0 / mde) ** 2
        d = np.ceil(n / (dq * 0.5))
        days.append(min(d, 365))
    axes[1].plot(mde_range, days, label=f"{dq} queries/day")
axes[1].axhline(y=30, color="red", linestyle="--", alpha=0.5, label="30일 한계")
axes[1].set_xlabel("MDE (Relevance Score 차이)")
axes[1].set_ylabel("필요 실험 기간 (일)")
axes[1].set_title("트래픽별 실험 기간")
axes[1].legend()
axes[1].grid(True, alpha=0.3)
axes[1].set_ylim(0, 120)

plt.tight_layout()
plt.show()

6 의사결정 프레임워크

일일 트래픽 파악
    ↓
MDE 설정 (비즈니스 의미 기준)
    ↓
표본 크기 계산
    ↓
실험 기간 = 표본 크기 / (일일 트래픽 × 분할 비율)
    ↓
┌─ 기간 ≤ 2주? → 표준 A/B 진행
├─ 기간 2~4주? → Sequential testing 적용 (조기 종료 가능)
├─ 기간 1~2개월? → 분산 감소 + MDE 재조정
└─ 기간 > 2개월? → 오프라인 평가로 대체, 또는 MDE를 크게 높임

7 관련 주제

선행 지식

시리즈 다음 포스트

다른 카테고리 연결

Subscribe

Enjoy this blog? Get notified of new posts by email: