見出し画像

atama plusにおけるA/Bテスト基盤構築の歩み

atama plusアルゴリズムチームの安本(@myasumoto)です。

atama plusのアルゴリズムチームのミッションは、「生徒の学習体験の根幹を成すレコメンドエンジンの改善によって、生徒の学びの最大化に貢献する」です。このミッションの達成に向け、日々レコメンドエンジンの改善を行っています。

改善の結果として実際に効果があったかを検証する「効果検証」は、その中でも大事なプロセスとなります。atama plusでは、A/Bテストを導入することで、定量的な効果検証を行っています。

アルゴリズムチームでは、アルゴリズムの改善から効果検証までのサイクルを素早く、かつ高い信頼性で回せるようにするために、A/Bテストの基盤を構築しています。
今回の記事では、基盤を構築する際に検討した3つの観点について、実際にどのような検討を行い、結果としてどのような結論に至ったかを紹介できればと思います。

また、atama plusではこれまでに様々な実験をしてきましたが、その中で失敗もありました。その失敗と改善に向けた取り組みについても、最後に少し触れたいと思います。

  1. 基盤に求められる要件の整理

  2. SaaS / OSS / 内製化?

  3. 実験のクオリティを担保するための仕組み作り

  4. これまでに経験した失敗とそこから得られた教訓

1. 基盤に求められる要件の整理

 まずはA/Bテスト基盤に関係するステークホルダーとその役割を整理しました。

プロダクトオーナー/学習体験プランナー(*)
A/Bテストを企画する。
効果検証の結果を判断する。
 (*)学習体験プランナーの詳細はこちら

データサイエンティスト
学習体験プランナーがA/Bテストの企画を行うのを支援する。
効果検証に必要なデータ量や期間の見積もり、監視するメトリクスの設定、有意差検定などを行う

QA
A/Bテストのテストを行う。

次に、各ステークホルダーへのヒアリングを通して、要件を整理していきました。

整理した要件

特筆すべき点としては、ステークホルダーによって要件は様々あり得る、ということでしょうか。例えば、データサイエンティストの観点では、分析のしやすさからBQとの親和性が、QAの観点では、テスタビリティに関する要望がありました。

POと一緒にこれらの要件に対して優先順位を付けた上で、次にこれらの要件を実現するための方法について検討を進めました。


2. SaaS / OSS / 内製化?

1.で整理した要件を実現するための方法として、SaaSの利用、OSSの活用、内製での開発の3つの選択肢を考え、それぞれについて調査・検討を行いました。

通常、既に完成度高くできているものがある場合は、管理コスト削減のため、なるべくSaaSの利用を検討し、それが難しければ、車輪の再発明を防ぐためにOSSの活用を検討し、それも難しい場合には内製化、という検討フローになるかと思いますが、今回も同様に、この順番で検討を行いました。

なお、通称カバ本として有名な、「A/Bテスト実践ガイド」の勉強会を社内で開催しており、第4章「実験のプラットフォームと文化」は、この検討の際に大いに参考にさせて頂きました。


SaaS

SaaSのメリットとしては、基盤の開発に関するコストの一切が不要となることです(代わりに利用料を支払う訳ですが)。

A/Bテストを支援するSaaSは世の中に様々あるようですが、フルスタックな機能を備えたものの1つとしてOptimzelyがあります。Optimizelyには、実験の対象範囲を段階的に拡大させる機能や、実験のコントロール機能、複数実験の管理機能など、多数の機能が実装されています。

前述した要件と比較したところ、ほぼ全ての要件を満たしていましたが、外部サービスという性質上、何点かはatama plus側での実装が不可避な部分がありました。

  • 「#2 対象ユーザーの抽出」については、ユーザーの属性情報がatama plusのサービスの中にしかないため、必要な場合はアプリケーション側のロジックで書く必要がありました。

  • 「#5 BQとの親和性」ついては、Optimizelyでは振り分け結果や実験結果を保持し外部サービスと連携するような機能はないため、基本的には自前でテーブルを持って振り分けの結果や実験結果を保存する、といった処理を書く必要がありました。

一方、atama plusでは現状、バックエンド周りでの実験で利用する想定ですが、将来的にフロントエンド含めて実験をすることになれば、フロント側の言語に対応したSDKも存在するため、対応しやすい、というメリットもありそうです。


OSS

OSSを活用することができれば、開発に関するコストを低減することができます。ここではatama plusのバックエンドで利用しているDjangoと組み合わせることができる、3つのA/Bテストフレームワークについて調査しました。


1. proctor

indeedが開発しているA/Bテストフレームワークで、開発も活発なように見えます。
フレームワーク自体はjavaで書かれていますが、Djangoから使えるようにするライブラリがあります。具体的には、REST APIとしてProctorの機能を公開した上で、そのAPIに対してクエリを発行し、振り分け情報などを取得する形になります。

2. Django-Experiments

Djangoに簡単に組み込むことができるのが利点ですが、DBのバックエンドがredis前提なのと、Django1.10にしか対応していない、という課題もありました。

3. ab

ユーザーを振り分ける機能だけを提供する、シンプルなフレームワーク。他にもオプションとして、多腕バンディットアルゴリズムも実装されています。


検討結果

  • SaaSのOptimizelyはとてもよい選択肢の1つでしたが、前述した通りアプリケーション側でもある程度コードを書く必要があること、スモールスタートで実験を実施していくのに対しては、利用料が高いことが予想されました。

  • OSSのproctorは機能がリッチで要件もほぼ満たしていましたが、専用のサーバーを立てる必要があり、インフラ管理コストの観点から避けたいと考えました。

  • 他のOSSについては、専用のサーバーが不要で組み込みが簡単そうでしたが、一方で更新が止まっていたり、機能がシンプルすぎるためほぼ自前で実装する必要がある、といった点で採用を見送りました。

  • 以上の検討結果より、最終的には、SaaSやOSSの機能を参考にしながら、内製で開発することに決定しました。


3. 実験のクオリティを担保するための仕組み作り

atama plusでは、A/Bテストを企画する人によって検討の観点がばらつくことを防ぐために、あらかじめ検討が必要な観点を並べた実験テンプレートを作成しています。このテンプレート作成は、データサイエンティストの@njunが主体となって推進しています。

企画時に検討が必要な観点としては、たとえば以下のようなものがあります。

A/Bテストにおける仮説
A/Bテストを企画するに至った仮説。A/Bテストは実験であり、基本的にはこの仮説が正しいかどうかを実験によって明らかにする、というものです。

実験期間
その実験期間中に十分なサンプルサイズが得られるかを考えて決めます。

評価指標(ゴールメトリクス)
モニタリングするメインのメトリクス。このメトリクスが優位に改善している場合に、A/Bテストは成功したといえます。

不変指標(ガードレールメトリクス)
メインのメトリクス以外にモニタリングすべきメトリクス。このメトリクスが通常の値から大きくずれた場合には、たとえゴールメトリクスが改善していても、仮説を疑う必要が出てきます。


テンプレートのサンプルの一部を以下に掲載します。テンプレートはNotion上に作成されており、実験の計画だけでなく、実験結果も同じページに記載することで、情報を一元管理しています。

ちなみに資料上部のお天気マークは、A/Bテストが成功したことを示しています!(もちろん失敗したA/Bテストも沢山あります!学習体験とは想像以上に奥が深いものです。)

またこのドキュメントを、A/Bテストに関わる全てのステークホルダーが見れるようにし、このドキュメントを介してコミュニケーションが取れるようにしました。これにより、ステークホルダーの間で認識のずれが発生することを防いでいます。

A/Bテストのテンプレート(サンプル)

4. これまでに経験した失敗とそこから得られた教訓

実験には失敗がつきものです。最後に、atama plusが実験をする中で失敗し、そこから得られた教訓についていくつか共有できればと思います。

教訓1. A/Bテストのコードは、テスト終了後のことを考えて書くべし

A/Bテストが終わり、A/Bどちらかのロジックに倒す場合に、バグが混入してしまう、ということがありました。これは、A/Bテストを実装する際に、少し複雑に書いてしまったことが原因でした。

得られた教訓:A/Bテストの分岐ロジックは、テスト終了後にどちらかに倒すのが簡単になるように書いておくこと。できればコードの削除だけで対応できるのが望ましい。

たとえば、以下のようなコードはよい例です。Aに倒す場合は、if/else文と、Bの場合のブロックを消すだけで対応できます。

if group == A:
    func(1)
    func(2)
elif group == B:
    func(3)
    func(4)

一方、これを少しスマートに、以下のように書き直すとどうでしょうか。

args_by_group = {A: [1, 2], B: [3, 4]}

for arg in args_by_group[group]:
    func(arg)

この時点では少しスマートに見えなくもないですが、A/Bテストが終わって片方に倒すときには、argsを辞書で持つ意味がなくなり、プログラムの構造に若干のリファクタが発生します。この際にバグが混入する可能性があります。


教訓2. A/Bテストは、終了時期を事前に必ず決めておくべし

期間は基本的にはサンプルサイズなどから事前に決めておくべきものですが、どの程度のデータがくるか事前に見積もることが難しい状況において、実験開始時点では終了時期が見積もれない場合があるかと思います。
ただ、そのような状況であっても、終了時期はあらかじめ決めた上で、その時期がくると自動で実験が終了するように実装しておくのがよいと考えます。

実験期間が1ヶ月にもなり、他に重要なタスクが舞い込んでくると、実験していたことを忘れてしまう可能性があります。atama plusでも似た事例が発生したことがありましたが、このようなリスクを見越して、終了時期を設定していたので、気づいた頃には既に実験が終了していました。もしネガティブなインパクトがある実験が忘れ去られていたら、と思うと、ヒヤヒヤする出来事でした。

得られた教訓:実験の終了時期を必ず決め、その時期がきたら実験が自動的に終了するような仕組みを実装しておくこと。


教訓3. A/B群への振り分け方法はランダムにすべし

一番最初の実験で、A/Bテストの基盤がまだそれほど整っていない時に、ユーザーIDの偶数/奇数で振り分けを行ったことがありました。
ただ、同じ振り分け方法でA/Aテストを実施したところ、有意差が出てしまい、ランダム化の条件が満たされていないことが判明した、ということがあり、以降は必ずランダムに振り分けを行うように実装しています。

得られた教訓:A/B群への振り分け方法は必ずランダムに行うこと。心配な場合はA/Aテストによって検証を行うこと。

いかがでしたでしょうか。
atama plusではこれからも、よりA/Bテストを効率的に、かつ精度高く実施できるように、継続的にA/Bテスト基盤の改善を進めていく予定です。なるべく早く、常に10件くらいの実験が並行して走っている、くらいの状態にまでスケールさせていきたいですね。

この記事が、これからA/Bテストを始めようとされている方の少しでもお役に立てると嬉しいです。それでは、よいA/Bテストライフを!



▼atama plus開発チームに関心を持っていただいた方へ
各種SNS(Twitterやzenn、note)で情報発信しています。
よろしければご覧ください。
https://linktr.ee/atama_plus_dev