我们刚刚开始为基于 Django 的项目进行 A/B 测试。我能否获得有关此 A/B 测试的最佳实践或有用见解的一些信息。
理想情况下,每个新的测试页面都将通过一个参数来区分(就像 Gmail 一样)。mysite.com/?ui=2 应该给出一个不同的页面。因此,对于每个视图,我都需要编写一个装饰器来根据“ui”参数值加载不同的模板。而且我不想在装饰器中硬编码任何模板名称。那么 urls.py url 模式将如何呢?
我们刚刚开始为基于 Django 的项目进行 A/B 测试。我能否获得有关此 A/B 测试的最佳实践或有用见解的一些信息。
理想情况下,每个新的测试页面都将通过一个参数来区分(就像 Gmail 一样)。mysite.com/?ui=2 应该给出一个不同的页面。因此,对于每个视图,我都需要编写一个装饰器来根据“ui”参数值加载不同的模板。而且我不想在装饰器中硬编码任何模板名称。那么 urls.py url 模式将如何呢?
在深入研究代码之前退后一步并抽象 A/B 测试试图做什么是很有用的。我们到底需要什么来进行测试?
考虑到这一点,让我们考虑实施。
目标
当我们考虑网络上的目标时,通常是指用户到达某个页面或完成特定操作,例如成功注册为用户或进入结帐页面。
在 Django 中,我们可以通过几种方式对其进行建模——也许在视图中天真地在达到目标时调用函数:
def checkout(request):
a_b_goal_complete(request)
...
但这无济于事,因为我们必须在需要的任何地方添加该代码——另外,如果我们使用任何可插入的应用程序,我们不想编辑它们的代码来添加我们的 A/B 测试。
我们如何在不直接编辑视图代码的情况下引入 A/B 目标?中间件呢?
class ABMiddleware:
def process_request(self, request):
if a_b_goal_conditions_met(request):
a_b_goal_complete(request)
这将使我们能够在网站的任何地方跟踪 A/B 目标。
我们如何知道目标的条件已经满足?为了便于实施,我建议我们知道当用户到达特定 URL 路径时,目标已经满足其条件。作为奖励,我们可以在不弄脏视图的情况下测量这一点。回到我们注册用户的例子,我们可以说当用户到达 URL 路径时这个目标已经实现:
/注册完成
所以我们定义a_b_goal_conditions_met
:
a_b_goal_conditions_met(request):
return request.path == "/registration/complete":
路径
当考虑 Django 中的路径时,很自然地会想到使用不同模板的想法。是否还有其他方法还有待探索。在 A/B 测试中,您可以在两个页面之间做出微小的差异并衡量结果。因此,最佳实践应该是定义一个单一的基本路径模板,所有通往目标的路径都应从该模板扩展。
应该如何渲染这些模板?装饰器可能是一个好的开始——在 Django 中最好的做法是在template_name
视图中包含一个参数,装饰器可以在运行时更改此参数。
@a_b
def registration(request, extra_context=None, template_name="reg/reg.html"):
...
你可以看到这个装饰器要么自省被包装的函数并修改template_name
参数,要么从某个地方(如模型)查找正确的模板。如果我们不想将装饰器添加到每个函数中,我们可以将其作为 ABMiddleware 的一部分来实现:
class ABMiddleware:
...
def process_view(self, request, view_func, view_args, view_kwargs):
if should_do_a_b_test(...) and "template_name" in view_kwargs:
# Modify the template name to one of our Path templates
view_kwargs["template_name"] = get_a_b_path_for_view(view_func)
response = view_func(view_args, view_kwargs)
return response
我们还需要添加一些方法来跟踪哪些视图正在运行 A/B 测试等。
用于将观众发送到路径的系统
理论上这很容易,但是有很多不同的实现,所以不清楚哪一个是最好的。我们知道一个好的系统应该在路径上均匀地划分用户 - 必须使用一些哈希方法 - 也许你可以使用 memcache 计数器的模数除以路径的数量 - 也许有更好的方法。
记录测试结果的系统
我们需要记录有多少用户走上了哪条路径——当用户达到目标时,我们还需要访问这些信息(我们需要能够说出他们走哪条路径来满足目标条件)——我们'将使用某种模型来记录数据,并使用 Django 会话或 Cookie 来保存路径信息,直到用户满足目标条件。
结束的想法
我已经给出了很多在 Django 中实现 A/B 测试的伪代码——以上绝不是一个完整的解决方案,而是为在 Django 中创建一个可重用的 A/B 测试框架的良好开端。
作为参考,您可能想在 GitHub 上查看 Paul Mar 的七分钟 A/B - 它是上述的 ROR 版本! http://github.com/paulmars/seven_minute_abs/tree/master
更新
在对 Google 网站优化器的进一步思考和调查中,很明显上述逻辑存在巨大漏洞。通过使用不同的模板来表示路径,您会破坏视图上的所有缓存(或者如果视图被缓存,它将始终提供相同的路径!)。相反,我不使用路径,而是窃取 GWO 术语并使用以下思想Combinations
- 这是模板更改的一个特定部分 - 例如,更改<h1>
站点的标签。
该解决方案将涉及将呈现为 JavaScript 的模板标签。当页面在浏览器中加载时,JavaScript 向您的服务器发出请求,该请求获取可能的组合之一。
这样,您可以在保留缓存的同时测试每个页面的多个组合!
更新
模板切换仍有空间——比如你引入了一个全新的主页并想测试它与旧主页的性能——你仍然想使用模板切换技术。要记住的是,您必须想办法在 X 个页面的缓存版本之间进行切换。为此,您需要覆盖标准缓存中间件,以查看它们是否是在请求的 URL 上运行的 A/B 测试。然后它可以选择正确的缓存版本来显示!!!
更新
使用上面描述的想法,我实现了一个用于基本 A/B 测试 Django 的可插拔应用程序。你可以从 Github 上下载它:
如果您使用建议的 GET 参数 ( ?ui=2
),那么您根本不必触摸urls.py。你的装饰师可以检查request.GET['ui']
并找到它需要的东西。
为了避免硬编码模板名称,也许您可以包装视图函数的返回值?除了返回 render_to_response 的输出,您可以返回一个元组(template_name, context)
并让装饰器修改模板名称。这样的事情怎么样?警告:我尚未测试此代码
def ab_test(view):
def wrapped_view(request, *args, **kwargs):
template_name, context = view(request, *args, **kwargs)
if 'ui' in request.GET:
template_name = '%s_%s' % (template_name, request.GET['ui'])
# ie, 'folder/template.html' becomes 'folder/template.html_2'
return render_to_response(template_name, context)
return wrapped_view
这是一个非常基本的示例,但我希望它能够传达这个想法。您可以修改有关响应的其他一些内容,例如向模板上下文添加信息。您可以使用这些上下文变量与您的站点分析集成,例如 Google Analytics。
作为奖励,如果您决定停止使用 GET 参数并移动到基于 cookie 等的东西,您可以在未来重构这个装饰器。
更新如果您已经编写了很多视图,并且不想全部修改它们,则可以编写自己的render_to_response
.
def render_to_response(template_list, dictionary, context_instance, mimetype):
return (template_list, dictionary, context_instance, mimetype)
def ab_test(view):
from django.shortcuts import render_to_response as old_render_to_response
def wrapped_view(request, *args, **kwargs):
template_name, context, context_instance, mimetype = view(request, *args, **kwargs)
if 'ui' in request.GET:
template_name = '%s_%s' % (template_name, request.GET['ui'])
# ie, 'folder/template.html' becomes 'folder/template.html_2'
return old_render_to_response(template_name, context, context_instance=context_instance, mimetype=mimetype)
return wrapped_view
@ab_test
def my_legacy_view(request, param):
return render_to_response('mytemplate.html', {'param': param})
贾斯汀的回答是对的……我建议你投票给那个,因为他是第一个。如果您有多个视图需要此 A/B 调整,他的方法特别有用。
但是请注意,如果您只有少数视图,则不需要装饰器或更改 urls.py。如果您将 urls.py 文件保持原样...
(r'^foo/', my.view.here),
...您可以使用 request.GET 来确定请求的视图变体:
def here(request):
variant = request.GET.get('ui', some_default)
如果您想避免为单个 A/B/C/etc 视图硬编码模板名称,只需在模板命名方案中使它们成为约定(正如 Justin 的方法也推荐的那样):
def here(request):
variant = request.GET.get('ui', some_default)
template_name = 'heretemplates/page%s.html' % variant
try:
return render_to_response(template_name)
except TemplateDoesNotExist:
return render_to_response('oops.html')
基于 Justin Voss 的代码:
def ab_test(force = None):
def _ab_test(view):
def wrapped_view(request, *args, **kwargs):
request, template_name, cont = view(request, *args, **kwargs)
if 'ui' in request.GET:
request.session['ui'] = request.GET['ui']
if 'ui' in request.session:
cont['ui'] = request.session['ui']
else:
if force is None:
cont['ui'] = '0'
else:
return redirect_to(request, force)
return direct_to_template(request, template_name, extra_context = cont)
return wrapped_view
return _ab_test
使用代码的示例函数:
@ab_test()
def index1(request):
return (request,'website/index.html', locals())
@ab_test('?ui=33')
def index2(request):
return (request,'website/index.html', locals())
这里发生了什么: 1. 传递的 UI 参数存储在 session 变量中 2. 每次加载相同的模板,但是一个上下文变量 {{ui}} 存储了 UI id(您可以使用它来修改模板) 3.如果用户在没有 ?ui=xx 的情况下进入页面,那么在 index2 的情况下,他将被重定向到 '?ui=33',在 index1 的情况下,UI 变量设置为 0。
我使用 3 从主页重定向到 Google 网站优化器,然后使用适当的 ?ui 参数重定向回主页。
您还可以使用 Google Optimize 进行 A/B 测试。为此,您必须将 Google Analytics 添加到您的网站,然后当您创建 Google Optimize 实验时,每个用户都会获得一个带有不同实验变体的 cookie(根据每个变体的权重)。然后,您可以从 cookie 中提取变体并显示应用程序的各种版本。您可以使用以下代码段来提取变体:
ga_exp = self.request.COOKIES.get("_gaexp")
parts = ga_exp.split(".")
experiments_part = ".".join(parts[2:])
experiments = experiments_part.split("!")
for experiment_str in experiments:
experiment_parts = experiment_str.split(".")
experiment_id = experiment_parts[0]
variation_id = int(experiment_parts[2])
experiment_variations[experiment_id] = variation_id
但是有一个 django 包可以很好地与 Google Optimize 集成:https ://github.com/adinhodovic/django-google-optimize/ 。
这是一篇关于如何使用该软件包以及 Google Optimize 如何工作的博客文章:https ://hodovi.cc/blog/django-b-testing-google-optimize/ 。