A/Bテストは「2つ見せて比べる」だけに見えますが、
実装と判定の両方に落とし穴があります。実際に踏んだものを整理します。
振り分けは決定的に行う
リクエストごとに乱数で決めると、同じ人が毎回違うものを見ます。
import hashlib
def variant(user_id: int, experiment: str) -> str:
h = hashlib.md5(f"{experiment}:{user_id}".encode()).hexdigest()
return "B" if int(h[:8], 16) % 2 else "A"
ユーザーIDと実験名からハッシュを取ると、
同じ人には常に同じ変種が出ます。実験名を含めるのがポイントで、
これが無いと複数の実験で同じ偏りが発生します。
未ログインの扱い
ログイン前後で振り分けが変わると、データが汚れます。
Cookie に識別子を持たせ、ログイン後も引き継ぎます。
aid = request.COOKIES.get("aid") or uuid.uuid4().hex
response.set_cookie("aid", aid, max_age=60*60*24*90)
記録するのは「割り当て」と「成果」
class Assignment(models.Model):
experiment = models.CharField(max_length=40)
subject = models.CharField(max_length=64) # user_id or aid
variant = models.CharField(max_length=8)
assigned_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = [("experiment", "subject")]
割り当てを記録した人だけを分母にするのが重要です。
「全訪問者」を分母にすると、実験対象外の人が混ざります。
途中で覗いて止めない
最もやりがちな失敗です。
3日目: Bが勝っている → 終了しよう
これは有意水準を大きく歪めます。何度も覗けば、
差が無くても「差がある」ように見える瞬間が必ず訪れます。
対策は単純で、開始前に期間とサンプル数を決め、それまで見ないことです。
EXPERIMENT = {
"name": "cta_wording",
"min_subjects_per_variant": 1000,
"min_days": 14,
}
曜日と季節の影響を排除する
平日と休日で行動が変わるサービスは多くあります。
最低1週間、できれば2週間回して曜日を一巡させます。
複数の実験を同時に走らせるとき
同時に走らせること自体は可能ですが、
同じ画面の同じ要素を対象にする実験は同時に走らせません。
効果が混ざって解釈できなくなります。
差が無いという結果も価値がある
「有意差なし」は失敗ではありません。
その変更に工数をかける必要が無いという重要な情報です。
実務では「差が無かったので、より効果のありそうな別の施策に移る」
という判断ができるようになるのが最大の価値でした。
まとめ
- 振り分けはハッシュで決定的に(実験名を含める)
- 未ログインは Cookie で識別し、ログイン後も引き継ぐ
- 分母は「割り当てを記録した人」
- 開始前に期間とサンプル数を決め、途中で覗いて止めない
- 最低1〜2週間回して曜日の影響を消す
- 「差が無い」も有益な結果として扱う