Gunosy Tech Blog

Gunosy Tech Blogは株式会社Gunosyのエンジニアが知見を共有する技術ブログです。

AndroidアプリにおけるA/Bテストのための実装

はじめに

こんにちは。今年10月に入社したグノシー事業部の高橋(@tkhs0604)です。
先週、胃腸炎に罹りました。来年の抱負は「健康第一」にしたいと思います。

この記事はGunosy Advent Calendar 2018、20日目の記事です。昨日の記事はUTさん(@mocyuto)の「まだログイン認証で消耗してるの? ~ALBで簡単認証機構~」でした。

前職はSIerで、入社から約1年は営業をしていましたが、その後エンジニアに社内ジョブチェンジし、WebアプリとAndroid/iOSアプリの開発に携わっていました 。
転職活動中にGunosyでは施策の検証を毎日データを見て行っているという話を聞き、それが当時の僕にとっては衝撃的でした。感性ではなくデータに基づきユーザと向き合う実直な姿勢に魅了され、入社を決意しました。

今回は、その取り組みのコアを担う「A/Bテスト」について書きたいと思います。

A/Bテストによる検証の流れ

グノシーアプリでは、打ち出す施策に対しほぼ必ずA/Bテストによる検証を行なっています。大まかな流れとしては以下のようになります。

1. 施策に対する仮説を立てる
   ex) ◯◯することで、起動時間は短くなるが、毎日起動してくれるようになる
2. 仮説検証のための指標とその情報を取得するログの設計を行う
   ex) DAU、セッション時間、記事のインプレッション、クリック数、ユーザ継続率…
3. A/Bテストを行うユーザを抽出し、各ユーザのデータを収集する
  - Treatment:施策の機能が提供されるユーザ
  - Control:従来の機能が提供されるユーザ
4. 2.で定めた指標をTreatment/Control間で比較し、
   有意な差が出た場合は対象範囲を広げる
5. 上記3-4.を繰り返し、100%適用or撤退を判断する

簡単なイメージとしては以下のようになります。

f:id:tkhs0604:20181217164751p:plain
A/Bテストの対象ユーザを決定する
f:id:tkhs0604:20181217164912p:plain
Treatment/Control間の指標を比較し有効性を検証する

2.のログ設計に関しては岡田さんの「LUCRAの分析を支えるモバイルアプリのログ設計と実装」をご覧ください。グノシーアプリにおいても概ね同様の構成になっています。

Androidアプリ側の実装

ここからは、グノシーアプリにおいてA/Bテストを行うためのAndroidアプリ側の実装について説明します。

まず、施策名の一覧をenumで定義します。

enum class ABTest(
    val key: String, 
    val defaultValue: Boolean = false
) {
    Policy1("policy_1"),
    Policy2("policy_2"),
    Policy3("policy_3"),
    …
}

次に、各施策をユーザに適用するか否かの情報を取得するためのインタフェースを定義します。
なお、各情報はアプリ起動時にサーバから受け取り、それらをRepositoryに渡す形にしています。

interface IABTestRepository {

    // 各施策をユーザに適用するか否かの情報を格納するMap
    var values: Map<String, Boolean>

    val isPolicy1Applied: Boolean
    val isPolicy2Applied: Boolean
    val isPolicy3Applied: Booleanfun getValue(abTest: ABTest): Boolean
}

サーバから受け取った各情報はキャッシュとSharedPreferencesに保存します。
キャッシュはファイルアクセス回数削減のために行なっています。

class ABTestRepository
@Inject
internal constructor(
    val context: Context
) : IABTestRepository {
    private val preference = PreferenceManager.getDefaultSharedPreferences(context)
    private var cache: Map<String, Boolean>? = null

    override var values: Map<String, Boolean>
        get() = cache ?: emptyMap()
        set(value) {
            // キャッシュにA/Bテスト用のkey/valueを登録する
            cache = value

            ABTest.values().forEach { abTest ->
                // SharedPreferencesにA/Bテスト用のkey/valueを登録する
                preference.edit().putBoolean(abTest.key, value[abTest.key] ?: abTest.defaultValue)
            }
        }

    override val isPolicy1Applied = getValue(Policy1)
    override val isPolicy2Applied = getValue(Policy2)
    override val isPolicy3Applied = getValue(Policy3)
    …

    override fun getValue(abTest: ABTest): Boolean {
        // キャッシュからA/Bテスト用のkeyに対応するvalueを取得する
        // キャッシュから値が取得できなければ、SharedPreferencesから取得する
        return cache?.get(abTest.key) ?: preference.getBoolean(abTest.key, abTest.defaultValue)
    }

}

あとはこれらの情報を用いてA/Bテスト用の実装を行い、指標取得のためにログ処理を追加するだけです。

指標の比較

以前はRedashでグラフや値を確認していましたが、社内メンバーが最近Slackで集計内容を届くようにしてくださったので、検証がさらに簡便になりました。
詳細は割愛しますが、各指標のいくつかの統計量を見て施策が上手くいっているかを検証しています。

おわりに

初めて技術ブログなるものを書いたので、至らない部分もあったかと思いますが、何かの参考になれば幸いです。
明日は@cou_zさんの「【年末年始に読みたい】Gunosyエンジニアが2018年に購入した書籍まとめ」です。お楽しみに!