MBaaS徹底入門――Kii Cloudでスマホアプリ開発

第14回A/Bテストによるアプリケーションの改善(実装編)

前回は、⁠なぜA/Bテストを行うのか」⁠ABテストを行う際に気をつけることはなにか」を解説しました。

第14回となる今回は、本連載で開発したチャットアプリケーションを題材として、Kii CloudでA/Bテストを実施する手順を解説します。チャットアプリケーションについては第5回第12回で詳細に解説しています。興味がある方はこちらもご覧ください。

今回使用したソースコードについては、GitHubで公開しています。こちらもあわせてご覧ください。

以降ではチャットアプリケーションの実装をある程度知っている前提で解説しています。そのため、第13回までの連載を事前に確認して頂くと分かりやすいと思います。

A/Bテストのシナリオと手順

前回例として示した、チャットアプリケーションの改善シナリオを引き続き使用します。本シナリオでのA/Bテストの目的は「ユーザにもっとチャットを楽しんでもらうため、スタンプ機能をもっと知って(使って)もらう」ことです。

前回立てた「スタンプ一覧表示ボタンをもっと目立つようにすることで、目的が達成できるのではないか」という仮説はそのままに、今回は「スタンプ一覧ボタンの色を変える」という改善をA/Bテストで検証する場合を考えてみます。

  • (パターンA⁠⁠:スタンプ一覧ボタンをデフォルトの色のままにする(現実装)
  • (パターンB⁠⁠:スタンプ一覧ボタンを赤色にする(改善実装)
パターンAのスタンプ一覧ボタン
パターンAのスタンプ一覧ボタン
パターンBのスタンプ一覧ボタン
パターンBのスタンプ一覧ボタン

パターンの良し悪しは、一覧ボタンの表示回数と実際にスタンプが投稿された回数を元に算出します。

上記のA/Bテストを実施するためには、2つの手順が必要です。これらを順に説明していきます。

  • A/Bテストの設定:作成したシナリオに基づいたA/Bテストを開発者ポータルから設定する。
  • クライアントの実装: 開発者ポータルで設定したA/Bテストの情報をKii Cloudから取得して、ユーザインターフェースに反映する。その後、⁠スタンプ一覧ボタンの表示」⁠スタンプの投稿」が行われるたびに、Kii Cloudにイベントを送信する。

A/Bテストの設定

開発者ポータルからA/Bテストを設定します。まず対象のアプリケーションをクリック後、⁠A/B tests⁠タブを選択しA/Bテストの一覧画面に移動します。A/Bテストをまだ追加していないため、一覧には何も表示されていません。

A/Bテスト一覧画面
A/Bテスト一覧画面

次に⁠Add⁠ボタンをクリックして、A/Bテスト作成画面を表示します。A/Bテストの設定項目は以下の通りです。

TitleA/Bテストの開発者ポータル上での表示名です。今回は「スタンプ一覧ボタンの色変更」と設定します。
DescriptionA/Bテストの概要です。今回は「A/Bテストのシナリオと手順」で記載した説明をそのまま使用します。
EventsA/Bテストのパターンを評価するためにクライアントから送信するイベントです。クライアントはここで設定したイベント名を使用して、イベントインスタンスの生成・送信を行います。具体的には以下の2つのイベントの名前を設定します。
ViewイベントConversionイベント:
「A/Bテストのシナリオと手順」で説明した (全試行回数)に相当するイベントです。今回は⁠stamp_button_viewedEvent⁠と設定します。「A/Bテストのシナリオと手順」で説明した (目的を達成した回数)に相当するイベントです。今回は⁠stamp_postedEvent⁠と設定します。
Distribution parametersA/Bテストの各パターン(パターンA/パターンB)の内容を設定します。具体的には次の2つを設定します。
Ratio of distributionVariables
各パターンを適用する割合です。今回はパターンA:50%、パターンB:50%とします。この場合、50%のユーザにはパターンA(一覧ボタンの色がデフォルトのまま)が、別の50%のユーザにはパターンB(一覧ボタンの色が赤色)が適用されます。各パターンで使用する変数です。具体的には(変数名⁠⁠、⁠変数のパターンAでの値⁠⁠、⁠変数のパターンBでの値)を定義します。クライアントは、この変数を用いて各パターン毎のユーザインターフェースを生成する必要があります。今回は、⁠変数名)=stamp_button_color、⁠変数のパターンAでの値)=default、⁠変数のパターンBでの値)=redとします。なお、今回は1つの変数しか設定していませんが、変数は複数設定可能です。
A/Bテスト作成画面
A/Bテスト作成画面

設定後、右下の⁠Save⁠ボタンをクリックしてA/Bテストを保存します。保存後に一覧画面を見ると、A/Bテストの作成が確認できます。

新規作成したA/Bテストには、⁠Draft⁠という表示がついています。⁠Draft⁠はA/Bテストのステータスを表しており、このステータスでは作成したA/Bテストは有効ではありません。A/Bテストを有効にするためには、ステータスを⁠Running⁠に変更する必要があります。

A/Bテスト保存後の一覧画面
A/Bテスト保存後の一覧画面

ステータスを変更するため、作成したA/Bテストのステータス(Draft)部分をクリックして詳細画面を開きます。詳細画面ではステータスの変更の他、設定内容の確認、A/Bテストの結果確認が行えます。今回はステータスの変更が目的のため、右下の⁠Start⁠ボタンをクリックします。

A/Bテスト詳細画面
A/Bテスト詳細画面

その後に一覧画面を見ると、A/Bテストのステータスの変更(Draft→Running)が確認できます。なお今回は解説のため、A/Bテストの作成後すぐに有効にしましたが、実際にはA/Bテストを開始するとき(例:キャンペーンの初日からA/Bテストを開始する)に有効にすればOKです。

A/Bテストのステータス変更
A/Bテストのステータス変更

クライアント側の実装

A/Bテストの実施のため、クライアント側では以下の3点を実装します。これらの実装はCloud SDK/Analytics SDKの両方を使用するため、これらのSDKを事前にプロジェクトに導入する必要があります(SDKの導入に関しては、第4回第10回を参照してください⁠⁠。

  • 「A/Bテストの設定」で作成したA/Bテストの情報を取得する。
  • 取得したA/Bテストの情報を元に、A/Bテストのパターンをユーザインターフェースに適用する。
  • A/Bテストの各パターンを評価するために、特定のタイミングでイベントを送信する。

A/Bテストの情報取得

A/Bテストの情報は、A/Bテストの対象となるコンポーネント(=スタンプ一覧ボタン)を表示する前に取得する必要があります。今回はサインイン/サインアップ後、チャットのメイン画面に遷移する前に取得処理を実行しています。PreMoveToChatMainTaskクラスが取得処理のタスクです。

public class SigninActivity extends FragmentActivity implements OnInitializeListener{
    …
    private class PostSigninTask extends ChatUserInitializeTask {
        …
        @Override
        protected void onPostExecute(Boolean result) {
            SimpleProgressDialogFragment.hide(getSupportFragmentManager());
            if (result) {
                // サインアップ処理が正常に行われた場合はメイン画面に遷移する
                new PreMoveToChatMainTask().execute();
            } else {
                ToastUtils.showShort(SigninActivity.this, "Unable to sign in");
            }
        }
    }
    …

    /**
     * Chatのメイン画面に遷移する前にA/Bテストの情報取得を行います。
     */
    private class PreMoveToChatMainTask extends ABTestInfoFetchTask {
        @Override
        protected void onPostExecute(KiiExperiment result) {
            moveToChatMain(result);
        }
    }
     …
}

取得処理の実態はABTestInfoFetchTaskクラスで定義されています。

public abstract class ABTestInfoFetchTask extends AsyncTask<Void, Void, KiiExperiment> {
    @Override
    protected KiiExperiment doInBackground(Void... params) {
        KiiExperiment experiment = null;
        try {
            experiment = KiiExperiment.getByID(ApplicationConst.ABTEST_ID);
        } catch (Exception e) {
            Logger.e("Fetching A/B test info is failed.", e);
        }
        return experiment;
    }
}

KiiExperiment.getByID(String)メソッドにA/BテストのIDを与えて、A/Bテストの情報を取得します。A/BテストのIDは作成時に自動で生成されるものであり、開発者ポータルのA/Bテスト詳細画面から確認できます。

A/BテストのIDの確認
A/BテストのIDの確認

パターンの適用

次に取得した情報を元に、スタンプ一覧ボタンを表示するActivity(ChatActivity)のonCreate(Bundle)メソッド内でA/Bテストのパターンを適用します。

public class ChatActivity extends FragmentActivity implements OnSelectStampListener {
    …
    private KiiExperiment experiment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        …
        // A/Bテストのパターン適用
        this.experiment = getIntent().getParcelableExtra(ChatActivity.INTENT_EXPERIMENT);
        if (experiment != null) {

            /*
             * 以下の場合、現実装(パターンA)をそのまま使用する。 
             * - 何らかのエラーが発生した場合 
             * - stamp_button_color="default"の場合
             */
            try {
                Variation variation = this.experiment.getAppliedVariation();
                Logger.d(String.format("Applied Pattern: %s", variation.getName()));

                // ABTEST_STAMP_BUTTON_COLOR=開発者ポータルで指定した変数名(”stamp_button_color”)
                String color = variation.getVariableSet().getString(
                        ApplicationConst.ABTEST_STAMP_BUTTON_COLOR);
                if (!ApplicationConst.ABTEST_DEFAULT.equals(color)) {
                    this.btnSelectEmoticon.setBackgroundColor(Color.parseColor(color));
                }
            } catch (Exception ignore) {
                Logger.d("A/B test pattern can't be applied. Thus, current implementation is used.");
            }
        } else {
            Logger.d("A/B test pattern can't be applied. Thus, current implementation is used.");
        }
        …
    }
}

KiiExperiment#getAppliedVariation()メソッドは、ログインユーザの情報と開発者ポータルで設定した⁠Ratio of distribution⁠(各パターンを適用する割合)を元に、パターンA/パターンBのどちらかのパターン(SDKでは、パターンをVaridationクラスとして扱う)を返します。また、ログインユーザの情報によりユーザに適用されるパターンは固定され、ログインのたびに別々のパターンが割りふられる、ということは起こりません。

パターンを取得した後はVariation#getVariableSet()メソッドで、開発者ポータルで設定した変数を含むJSONオブジェクトを取得します。この変数の値を用いて、各パターン毎のユーザインターフェースを実現することになります。上記のコードでは、変数⁠stamp_button_color⁠が⁠default⁠(パターンA)でない場合、変数の値からカラーコードを生成してスタンプ一覧ボタンの背景色を変更します(パターンB)。

A/Bテストのイベント送信

開発者ポータルで設定したイベントを、適切なタイミングでKii Cloudに送信する処理を実装します。

“stamp_button_viewedEvent⁠イベントは、画面遷移の結果として、スタンプ一覧ボタンが表示されかつクリック可能であるすべての場合に送信する必要があります。以下は、チャット画面(スタンプ一覧ボタンを含む)が表示されるたびにイベントを送信する処理を、ChatActivity#onResume()メソッド内に実装しています。これ以外に、⁠スタンプ一覧画面から処理を終えてチャット画面に制御が戻った場合」などに同様の処理を実装する必要があります。

public class ChatActivity extends FragmentActivity implements OnSelectStampListener,
        OnCancelStampDialogListner {
    …
    @Override
    protected void onResume() {
        super.onResume();
        …
        this.experiment = getIntent().getParcelableExtra(INTENT_EXPERIMENT);
        // スタンプ一覧ボタンが表示されて且つクリック可能である時、viewedEventを送信する
        onViewStampListButton();
        …
    }
    …
    @Override
    public void onViewStampListButton() {
        new SendABTestEventTask(ApplicationConst.ABTEST_STAMP_BUTTON_VIEWED_EVENT).execute();
    }
    …
}

“stamp_postedEvent⁠イベントは、スタンプの投稿/新規スタンプのアップロードが行われた後に送信する必要があります。以下は、スタンプの投稿についての実装です。ダイアログ(SelectStampDialogFragmentクラス)でスタンプが選択された後に、ChatActivity#onSelectStamp(ChatStamp)が呼ばれてイベント送信が行われます。同様の処理を、新規スタンプのアップロードに関しても実装しています。

public class ChatActivity extends FragmentActivity implements OnSelectStampListener,
        OnCancelStampDialogListner {
    …
    @Override
    public void onSelectStamp(ChatStamp stamp) {
        // スタンプ機能を利用した(スタンプ投稿/新規スタンプ追加)時、postedEventを送信する
        new SendABTestEventTask(ApplicationConst.ABTEST_STAMP_POSTED_EVENT).execute();
        // 選択されたスタンプをメッセージとしてバックグラウンドでKiiCloudに保存する
        new SendMessageTask(ChatMessage.createStampChatMessage(this.kiiGroup, stamp)).execute();
    }
    …
}

public class SelectStampDialogFragment extends DialogFragment implements
        LoaderCallbacks<List<ChatStamp>>, OnItemClickListener {
    …
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        // 選択されたスタンプをリスナー経由で通知する
        ChatStamp stamp = (ChatStamp) parent.getItemAtPosition(position);
        OnSelectStampListener selectListener = onSelectStampListener.get();
        if (selectListener != null && stamp != null) {
            selectListener.onSelectStamp(stamp);        
        }
        …
        dismiss();
    }
    …
}

イベントの送信処理の実体はChatActivity#SendABTestEventTaskクラスで定義しています。

public class ChatActivity extends FragmentActivity implements OnSelectStampListener,
        OnCancelStampDialogListner {
    …
    /**
     * A/Bテスト関係のイベントをバックグラウンドで送信します。
     */
    private class SendABTestEventTask extends AsyncTask<Void, Void, Boolean> {

        private String eventName;
        private KiiEvent event;

        private SendABTestEventTask(String eventName) {
            this.eventName = eventName;
            try {
                if (experiment != null) {
                    Variation variation = experiment.getAppliedVariation();
                    event = variation
                            .eventForConversion(KiiChatApplication.getContext(), eventName);
                }
            } catch (Exception ignore) {
                // eventがセットされない(null)であることを失敗とみなす。
            }
        }

        @Override
        protected Boolean doInBackground(Void... params) {
            if (event == null) {
                return false;
            }
            try {
                event.push();
                return true;
            } catch (IOException e) {
                e.printStackTrace();
                return false;
            }
        }
        …
    }

Variation#eventForConversion(Context, String)メソッドにより、イベントインスタンス(KiiEvent)の生成を行います。第2引数には開発者ポータルで設定したイベントの名前を渡します。生成したインスタンスのKiiEvent#push()メソッドを呼び出すことで、イベントの送信が行われます。

A/Bテストの終了

以上の設定が完了した後は、ユーザがスタンプ一覧画面を表示/クリックするたびにイベントの送信が行われます。送信されたイベントはKii Cloudで自動的に解析され、その結果は開発者ポータル上から確認できます。解析は1日1回行われて開発者ポータルに反映されるため、アップロードしたイベントが結果に反映されるまでには最大24時間のタイムラグが発生します。

A/Bテスト詳細画面の⁠Results⁠項目が解析結果です。解析結果の詳細な説明は、公式のドキュメントにまとめられています。

A/Bテストの解析結果
A/Bテストの解析結果

この結果を元に、現在の実装(パターンA⁠⁠、改善実装(パターンB)どちらを採用するかを決定します。

ただし、実際には解析結果の精度を高めるため、ある程度のイベント(大体1,000個以上)が集計できた後にどちらを採用するかを判断します。詳細は前述の公式ドキュメントをご確認ください。

以降は、改善実装を採用したとして説明します。

まず、右下の⁠Finish⁠をクリックしてA/Bテストの終了を宣言します。どちらのパターンを採用するかを確認するダイアログが表示されるため、パターンBを選択した後に’Stop Experiment⁠ボタンをクリックします。

採用するパターンの選択
採用するパターンの選択

すると、A/Bテストのステータスが⁠Running⁠から⁠Terminated⁠に変更され、⁠Results⁠項目にはパターンBが採用された旨が表示されます。

採用するパターンの選択
採用するパターンの選択

A/Bテストのステータスが⁠Terminated⁠の場合、クライアント側には常に採用したパターンが適用されます。コードレベルでは、KiiExperiment#getAppliedVariation()メソッドが常に採用したパターンに対応するVariationインスタンスを返すようになります。

// 「パターンの適用」で示したコード
public class ChatActivity extends FragmentActivity implements OnSelectStampListener {
    …
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        …
        this.experiment = getIntent().getParcelableExtra(ChatActivity.INTENT_EXPERIMENT);
        if (experiment != null) {
            try {

                // 常にパターンBに対応するVariationインスタンスを返す
                Variation variation = this.experiment.getAppliedVariation();
                ...
                // 常にパターンBのため、スタンプ一覧ボタンの色は必ず変更される
                String color = variation.getVariableSet().getString(
                        ApplicationConst.ABTEST_STAMP_BUTTON_COLOR);
                if (!ApplicationConst.ABTEST_DEFAULT.equals(color)) {
                    this.btnSelectEmoticon.setBackgroundColor(Color.parseColor(color));
                }
            } catch (Exception ignore) {
                Logger.d("A/B test pattern can't be applied. Thus, current implementation is used.");
            }
        } else {
            Logger.d("A/B test pattern can't be applied. Thus, current implementation is used.");
        }
        …
    }
}

まとめ

今回は、Kii CloudでA/Bテストを利用するための手順を解説しました。前回も触れましたが、A/Bテストは『実行すれば必ず効果が出るものではなく、自分の改善案を実施・分析してフィードバックループを素早く回すための手段』です。そのため、一回A/Bテストを実施するだけで終わらずに、A/Bテストの結果をフィードバックして改善を続けていくことが重要です。

おすすめ記事

記事・ニュース一覧