AとB、どっちの顔が好み?
A/Bテストとは、異なる2パターンのWebページを用意し、実際にユーザーに利用させて効果を比較するテストのこと。(ASCII.jpデジタル用語辞典)
ウェブサイトに訪れたユーザーの数千人に一人くらいに微妙に違うバージョンのページを見せて、通常バージョンとのクリック率の違いなどを比較する。もし通常より良い結果が出たらそれを通常版として採用し、さらに少し改変したものをテストする…
A/Bテストって、だいたいこんな感じの手法。
オバマ大統領が選挙に使ったことでも有名になった。
大規模なユーザーを想定した一般的なA/Bテストとはちょっと毛色が違うけど、
顔のモンタージュでA/Bテストっぽいことをやってみたのがコレ。
AとB、どっちか好きなほうの顔をクリック。
すると選んだほうの顔が残って、新しい選択肢が提示される。
次々選んでいくうちに、だんだんと理想の顔が出来上がるよ。
##仕組み
それでは実際に中身を見てみよう。
たったの150行。Yes,We can.
- まゆげ、目、鼻、口の画像をいくつか用意して番号をふる。
- 各パーツをランダムに選択する。
まずはサーバーサイド。Pythonのコード。
##ソース
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import webapp2
import os
import random
from google.appengine.ext.webapp import template
from google.appengine.api import memcache
class Abface(webapp2.RequestHandler):
def get(self):
self.draw()
def post(self):
if not self.request.get('choice') is None and self.request.get('choice')!="":
memcache.set("face",[int(i) for i in self.request.get('choice').split(",")], 3600)
memcache.set("sel",self.request.get('sel'), 3600)
self.draw()
def draw(self):
face1 = self.makeface()
face2=[i for i in face1]
r=random.randint(0,3)
face2[r] = int(random.choice("12345".replace(str(face2[r]), "")))
if memcache.get("sel")=="B":
face1,face2=face2,face1
template_values={
'brow1':'brow_' + str(face1[0]) + '.jpg',
'eye1':'eye_' + str(face1[1]) + '.jpg',
'nose1':'nose_' + str(face1[2]) + '.jpg',
'mouth1':'mouth_' + str(face1[3]) + '.jpg',
'brow2':'brow_' + str(face2[0]) + '.jpg',
'eye2':'eye_' + str(face2[1]) + '.jpg',
'nose2':'nose_' + str(face2[2]) + '.jpg',
'mouth2':'mouth_' + str(face2[3]) + '.jpg',
'face1':",".join(map(str,face1)),
'face2':",".join(map(str,face2)),
}
path = os.path.join(os.path.dirname(__file__), 'html/abface.html')
self.response.out.write(template.render(path, template_values))
def makeface(self):
face = memcache.get("face")
if face is None:
face = [random.randint(1,5) for i in range(4)]
memcache.set("face", face, 3600)
return face
app = webapp2.WSGIApplication([
('/abface.html', Abface)
], debug=True)
まず、ユーザーがアクセスした時に呼び出されるのが、get()メソッド。
get()メソッドはdraw()メソッドを呼び出す。以上。
で、draw()メソッドの中身。ここでページを生成してる。
まず1行目でmakeface()を呼び出す。たらい回されてばっかりだ。
makeface()。ここのキモはmemcache。
時間が経つと消えちゃうキャッシュ。
“face”というキーには現時点での一番良い顔が[1,3,4,5]
みたいな配列で入ってる(はず)。
これは眉:1番、目:3番、鼻:4番、口:5番という意味。
こんな感じでデータが入っている…かもしれないし、入ってないかもしれない。保証はない。
なぜならmemcacheはキャッシュだから消えてるかもしれない。
そこでfaceが空っぽならランダムに顔を生成して保存する。寿命は3600秒=1時間。
永久保存したいならちゃんとしたDBに保存しなきゃいけないけど、これはお遊びだから消えたら消えたでいい。
というわけでdraw()に戻る。
makeface()で作った顔を元に、眉目鼻口のどれかひとつをいじる。これがもう一つの選択肢。
ここでまたmemcacheの登場。前回選ばれた顔がBならば、顔1と顔2を入れ替える。
そしておもむろにテンプレートに値をセットしてクライアントに渡す。
これがサーバー側の顔表示の仕組み。
最後にpost()の説明。クライアントからABどちらを選んだか、選んだ顔の組み合わせを受け取り、memcacheにセットする。
続いてクライアント側。
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<title>ABface</title>
<style>
#canv1,#canv2 {
border: solid 3px #000;
border-radius: 25px 25px 25px 25px / 25px 25px 25px 25px;
border-color: #ff0000;
}
#canv2 {
border: solid 3px #000;
border-radius: 25px 25px 25px 25px / 25px 25px 25px 25px;
border-color: #0000ff;
}
#canv1:hover {
box-shadow: 0px 0px 20px 5px #ff0000
}
#canv2:hover {
box-shadow: 0px 0px 20px 5px #0000ff
}
#main {
width: 100%;
text-align : center
}
.hid{
display:none;
}
</style>
</head>
<body>
<div id="main">
<h1><span style="color:#ff0000">A</span> or <span style="color:#0000ff">B</span></h1>
<canvas id="canv1" width="150" height="150"></canvas>
<canvas id="canv2" width="150" height="150"></canvas>
</div>
<form action="/abface.html" method="post" id ="frm">
<input type="text" name="choice" id="choice" value="" style="display:none">
<input type="text" name="sel" id="sel" value="" style="display:none">
</form>
<img src="img/face/{{ brow1 }}" id ="brow_1" class="hid">
<img src="img/face/{{ eye1 }}" id ="eye_1" class="hid">
<img src="img/face/{{ nose1 }}" id ="nose_1" class="hid">
<img src="img/face/{{ mouth1 }}" id ="mouse_1" class="hid">
<img src="img/face/{{ brow2 }}" id ="brow_2" class="hid">
<img src="img/face/{{ eye2 }}" id ="eye_2" class="hid">
<img src="img/face/{{ nose2 }}" id ="nose_2" class="hid">
<img src="img/face/{{ mouth2 }}" id ="mouse_2" class="hid">
<script>
var ctx1 = document.getElementById('canv1').getContext('2d');
var ctx2 = document.getElementById('canv2').getContext('2d');
var face1 = "{{ face1 }}"
var face2 = "{{ face2 }}"
function makeface(){
ctx1.drawImage(document.getElementById('brow_1'), 0, 0)
ctx1.drawImage(document.getElementById('eye_1'), 0, 25)
ctx1.drawImage(document.getElementById('nose_1'), 0, 50)
ctx1.drawImage(document.getElementById('mouse_1'), 0, 110)
ctx2.drawImage(document.getElementById('brow_2'), 0, 0)
ctx2.drawImage(document.getElementById('eye_2'), 0, 25)
ctx2.drawImage(document.getElementById('nose_2'), 0, 50)
ctx2.drawImage(document.getElementById('mouse_2'), 0, 110)
}
window.onload = function(){
makeface()
$("#canv1").bind("click",function(){
ctx1.beginPath();
ctx1.lineWidth = 10;
ctx1.strokeStyle="#ff0000";
ctx1.arc(75, 75, 50, 0, Math.PI*2, false)
ctx1.stroke();
$("#choice").val(face1);
$("#sel").val("A");
$('#frm').submit();
});
$("#canv2").bind("click",function(){
ctx2.beginPath();
ctx2.strokeStyle="#0000ff";
ctx2.lineWidth = 10;
ctx2.arc(75, 75, 50, 0, Math.PI*2, false)
ctx2.stroke();
$("#choice").val(face2);
$("#sel").val("B");
$('#frm').submit();
});
}
</script>
</body>
</html>
まず見るべきはmakeface()。
canvasに画像をぺたぺた貼ってる。
2枚のcanvasにぺたぺた貼ってる。
IMG要素はサーバー側から受け取ったパーツ番号にそってURLを切り替えてる。
表示の部分はこんだけ。
次はcanvasをクリックした時のイベント。
なんか色々やってるけど見た目の問題だからほとんど無視していい。
重要なのは最後の3行。
選んだパーツをセットして、AかBかをセットして、そしてSubmit。
するとサーバー側ではpost()が呼ばれる。
以上。
これだけでABテストっぽいものが作れる。
##まとめ
パーツの種類増やしたり輪郭や髪型や配置なども選べたら夢が広がりそう。
素材は全部女性の顔で作ったのに、出来上がる顔は男の顔ばかり。
パーツ配置のバランスの問題かな。