有关基于Django项目的A/B测试的想法吗?

Non*_*-da 56 testing django ab-testing

我们刚刚开始为基于Django的项目进行A/B测试.我是否可以获得有关此A/B测试的最佳实践或有用见解的一些信息.

理想情况下,每个新的测试页面都将使用单个参数进行区分(就像Gmail一样).mysite.com/?ui=2应该给出不同的页面.因此,对于每个视图,我需要编写一个装饰器来根据'ui'参数值加载不同的模板.而且我不想在装饰器中硬编码任何模板名称.那么urls.py url模式将如何?

jb.*_*jb. 94

在深入研究代码之前,退一步并抽象A/B测试正在尝试做的事情是有用的.我们究竟需要进行什么测试?

  • 有条件的目标
  • 至少有两条不同的路径可以满足目标的条件
  • 用于向其中一个路径发送观众的系统
  • 用于记录测试结果的系统

考虑到这一点,让我们考虑实施.

目标

当我们考虑网络上的目标时,我们通常意味着用户到达某个页面或者他们完成了特定的操作,例如成功注册为用户或进入结帐页面.

在Django中,我们可以通过几种方式对其进行建模 - 可能在视图中天真地,在达到目标时调用函数:

    def checkout(request):
        a_b_goal_complete(request)
        ...
Run Code Online (Sandbox Code Playgroud)

但这没有用,因为我们必须在我们需要的任何地方添加代码 - 如果我们使用任何可插拔的应用程序,我们宁愿不编辑他们的代码来添加我们的A/B测试.

如何在不直接编辑视图代码的情况下引入A/B目标?中间件怎么样?

    class ABMiddleware:
      def process_request(self, request):
          if a_b_goal_conditions_met(request):
            a_b_goal_complete(request)
Run Code Online (Sandbox Code Playgroud)

这样我们就可以在网站的任何地方跟踪A/B目标.

我们怎么知道目标的条件得到满足?为了便于实现,我建议我们知道当用户到达特定的URL路径时,目标已满足条件.作为奖励,我们可以测量这一点,而不会让我们的手弄脏视图.回到我们注册用户的示例,我们可以说当用户到达URL路径时已达到此目标:

/注册完成

所以我们定义a_b_goal_conditions_met:

     a_b_goal_conditions_met(request):
       return request.path == "/registration/complete":
Run Code Online (Sandbox Code Playgroud)

路径

在考虑Django中的Paths时,很自然地会跳到使用不同模板的想法.是否有另一种方式还有待探索.在A/B测试中,您在两个页面之间做出微小差异并测量结果.因此,最佳做法是定义单个基本路径模板,目标的所有路径都应该从该模板扩展.

应该如何呈现这些模板?装饰器可能是一个好的开始 - 在Django中最好的做法是在template_name视图中包含一个参数,装饰器可以在运行时更改此参数.

    @a_b
    def registration(request, extra_context=None, template_name="reg/reg.html"):
       ...
Run Code Online (Sandbox Code Playgroud)

您可以看到这个装饰器要么内省包装函数,要么修改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
Run Code Online (Sandbox Code Playgroud)

我们还需要添加一些方法来跟踪哪些视图有A/B测试运行等.

用于沿路径发送观看者的系统

理论上这很容易,但是有很多不同的实现,所以不清楚哪一个是最好的.我们知道,一个好的系统应该分为用户均匀地沿着小路 - 一些散列法必须使用 - 也许你可以使用内存缓存计数器由路径的数目除以模 - 也许有更好的方法.

用于记录测试结果的系统

我们需要记录多少用户去了什么路径 - 当用户到达目标(我们需要能够说他们就什么路径到满足目标的条件),我们也需要访问这些信息 - 我们将使用某种模型来记录数据,并使用Django Sessions或Cookies来保留路径信息,直到用户满足目标条件.

闭幕思考

我已经给了很多的伪码实现在Django A/B测试 - 上面是决不是一个完整的解决方案,但对为在Django A/B测试可重复使用的框架,一个良好的开端.

作为参考,你可能想看看Paul Mar在GitHub上的七分钟A/B - 这是上面的ROR版本! http://github.com/paulmars/seven_minute_abs/tree/master


更新

关于Google网站优化工具的进一步反思和调查,很明显上述逻辑存在漏洞.通过使用不同的模板来表示路径,您可以破坏视图上的所有缓存(或者如果缓存视图,它将始终提供相同的路径!).相反,使用Paths,我会窃取GWO术语并使用Combinations- 这是模板更改的一个特定部分 - 例如,更改<h1>站点的标记.

该解决方案将涉及模板标记,这些模板标记将呈现为JavaScript.当页面在浏览器中加载时,JavaScript向您的服务器发出请求,该请求将获取其中一个可能的组合.

这样,您可以在保留缓存的同时每页测试多个组合!


更新

仍然存在模板切换的空间 - 例如,您引入了一个全新的主页,并希望在旧主页上测试它的性能 - 您仍然希望使用模板切换技术.需要记住的是,您必须找到一些方法来切换页面的X个缓存版本.为此,您需要覆盖标准缓存中间件,以查看它们是否是在请求的URL上运行的A/B测试.然后它可以选择正确的缓存版本来显示!


更新

使用上面提到的想法,我实现了一个可插拔的应用程序,用于基本的A/B测试Django.你可以把它从Github上拿下来:

http://github.com/johnboxall/django-ab/tree/master

  • 这是我在这个网站上看过的最好的帖子之一.好东西. (8认同)
  • 安提阿的神圣手榴弹,你不会停止,直到它完成. (3认同)

seb*_*ano 12

Django lean是A/B测试的一个很好的选择

http://bitbucket.org/akoha/django-lean/wiki/Home


Jus*_*oss 7

如果您使用像suggsted(?ui=2)那样的GET参数,那么您根本不必触摸urls.py.你的装饰师可以检查request.GET['ui']并找到它需要的东西.

为了避免硬编码模板名称,也许你可以从视图函数中包装返回值?您可以返回一个元组(template_name, context)并让装饰器破坏模板名称,而不是返回render_to_response的输出.这样的事怎么样?警告:我还没有测试过这段代码

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
Run Code Online (Sandbox Code Playgroud)

这是一个非常基本的例子,但我希望它可以得到这个想法.您可以修改有关响应的其他几个内容,例如向模板上下文添加信息.您可以使用这些上下文变量与您的网站分析集成,例如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})
Run Code Online (Sandbox Code Playgroud)