web-dev-qa-db-fra.com

Des réflexions sur les tests A/B dans les projets basés sur Django?

Nous venons tout juste de commencer les tests A/B pour notre projet basé sur Django. Puis-je obtenir des informations sur les meilleures pratiques ou des informations utiles sur ce test A/B.

Idéalement, chaque nouvelle page de test sera différenciée avec un seul paramètre (tout comme Gmail). mysite.com/?ui=2 devrait donner une page différente. Donc, pour chaque vue, je dois écrire à un décorateur pour charger différents modèles en fonction de la valeur du paramètre 'ui'. Et je ne veux pas coder en dur les noms de modèles dans les décorateurs. Alors, comment serait le motif urls.py?

57
None-da

Il est utile de prendre du recul et de résumer ce que les tests A/B tentent de faire avant de plonger dans le code. Qu'est-ce exactement aurons-nous besoin d'effectuer un test?

  • Un objectif qui a une condition
  • Au moins deux chemins distincts pour satisfaire à la condition de l'objectif
  • Un système d'envoi de téléspectateurs vers le bas l'un des chemins
  • Un système pour enregistrer les résultats du test

Dans cet esprit, pensons à la mise en œuvre.

L'objectif

Lorsque nous pensons à un objectif sur le Web, nous entendons généralement le fait qu'un utilisateur accède à une page donnée ou accomplit une action spécifique, par exemple s’enregistrer avec succès en tant qu’utilisateur ou accéder à la page de paiement.

Dans Django, nous pouvons modéliser cela de différentes manières - peut-être naïvement dans une vue, appeler une fonction chaque fois qu'un objectif est atteint:

    def checkout(request):
        a_b_goal_complete(request)
        ...

Mais cela n'aide pas, car nous devons ajouter ce code partout où nous en avons besoin. De plus, si nous utilisons des applications connectables, nous préférons ne pas modifier leur code pour ajouter notre test A/B.

Comment introduire des objectifs A/B sans modifier directement le code d'affichage? Qu'en est-il d'un middleware?

    class ABMiddleware:
      def process_request(self, request):
          if a_b_goal_conditions_met(request):
            a_b_goal_complete(request)

Cela nous permettrait de suivre les objectifs A/B n'importe où sur le site.

Comment savons-nous que les conditions d'un objectif ont été remplies? Pour faciliter la mise en œuvre, je suggérerais que nous sachions qu'un objectif a ses conditions remplies lorsqu'un utilisateur atteint un chemin d'URL spécifique. En prime, nous pouvons mesurer cela sans nous salir les mains à l'intérieur d'une vue. Pour revenir à notre exemple d’enregistrement d’un utilisateur, nous pourrions dire que cet objectif a été atteint lorsque l’utilisateur atteint le chemin de l’URL:

/Inscription complète

Nous définissons donc a_b_goal_conditions_met:

     a_b_goal_conditions_met(request):
       return request.path == "/registration/complete":

Chemins

Quand on pense à Paths dans Django, il est naturel de sauter à l’idée d’utiliser différents modèles. Reste à savoir s'il existe un autre moyen. Dans le test A/B, vous faites de petites différences entre deux pages et mesurez les résultats. Par conséquent, il est recommandé de définir un modèle de chemin d'accès de base unique à partir duquel tous les chemins d'accès à l'objectif devraient s'étendre.

Comment devrait rendre ces modèles? Un décorateur est probablement un bon début. Il est recommandé à Django d'inclure un paramètre template_name dans vos vues. Un décorateur pourrait modifier ce paramètre au moment de l'exécution.

    @a_b
    def registration(request, extra_context=None, template_name="reg/reg.html"):
       ...

Vous pouvez voir ce décorateur introspectionner la fonction encapsulée et modifier l'argument template_name ou rechercher les modèles appropriés quelque part (comme un modèle). Si nous ne voulions pas ajouter le décorateur à chaque fonction, nous pourrions l'implémenter dans le cadre de notre 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

Nous aurions également besoin d’ajouter un moyen de savoir quelles vues ont des tests A/B en cours, etc.

Un système pour envoyer des téléspectateurs par un chemin

En théorie, c'est facile, mais il y a beaucoup d'implémentations différentes, donc on ne sait pas laquelle est la meilleure. Nous savons qu'un bon système doit diviser les utilisateurs uniformément sur le chemin. Une méthode de hachage doit être utilisée. Vous pouvez peut-être utiliser le module du compteur memcache divisé par le nombre de chemins. Il existe peut-être un meilleur moyen.

Un système pour enregistrer les résultats du test

Nous devons enregistrer combien d'utilisateurs ont emprunté ce chemin - nous aurons également besoin d'accéder à ces informations lorsque l'utilisateur atteindra l'objectif (nous devons être en mesure de dire quel chemin il est venu pour remplir la condition de l'objectif) - nous J'utiliserai une sorte de modèle (s) pour enregistrer les données et soit des sessions Django, soit des cookies pour conserver les informations de chemin jusqu'à ce que l'utilisateur remplisse la condition de l'objectif.

Réflexions finales

J'ai donné beaucoup de pseudo-codes pour implémenter les tests A/B dans Django. Ce qui précède n'est en aucun cas une solution complète, mais un bon début pour la création d'un framework réutilisable pour les tests A/B dans Django.

Pour référence, vous pouvez consulter les Seven Minute A/Bs de Paul Mar sur GitHub - c'est la version ROR de ce qui précède! http://github.com/paulmars/seven_minute_abs/tree/master


Mise à jour

Une réflexion plus approfondie et une analyse approfondie de Google Website Optimizer montrent clairement que la logique ci-dessus présente des lacunes. En utilisant différents modèles pour représenter les chemins, vous annulez la mise en cache de la vue (ou si la vue est mise en cache, elle servira toujours le même chemin!). Au lieu d’utiliser Paths, j’aimerais voler la terminologie GWO et utiliser l’idée de Combinations - c’est une partie spécifique d’un modèle qui change - par exemple, changer la balise <h1> d’un site.

La solution impliquerait des balises de modèle qui rendraient au JavaScript. Lorsque la page est chargée dans le navigateur, JavaScript envoie une requête à votre serveur qui extrait l'une des combinaisons possibles.

De cette façon, vous pouvez tester plusieurs combinaisons par page tout en préservant la mise en cache!


Mise à jour

Il est toujours possible de changer de modèle (par exemple, si vous introduisez une page d'accueil entièrement nouvelle et souhaitez tester ses performances par rapport à l'ancienne page d'accueil), vous souhaitez toujours utiliser la technique de changement de modèle. La chose à garder à l'esprit est que vous allez devoir trouver un moyen de basculer entre un nombre X de versions en cache de la page. Pour ce faire, vous devez remplacer le middleware mis en cache standard pour savoir s'il s'agit d'un test A/B s'exécutant sur l'URL demandée. Ensuite, il pourrait choisir la bonne version en cache à afficher !!!


Mise à jour

En utilisant les idées décrites ci-dessus, j'ai implémenté une application enfichable pour les tests A/B de base de Django. Vous pouvez l'obtenir de Github:

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

95
jb.

Django Lean est une bonne option pour les tests A/B

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

12
sebastian serrano

Si vous utilisez les paramètres GET comme vous l'avez suggéré (?ui=2), vous ne devriez pas avoir à toucher à urls.py. Votre décorateur peut inspecter request.GET['ui'] et trouver ce dont il a besoin.

Pour éviter les noms de modèles de codage en dur, vous pourriez peut-être envelopper la valeur de retour à partir de la fonction d'affichage? Au lieu de renvoyer la sortie de render_to_response, vous pouvez renvoyer un tuple de (template_name, context) et laisser le décorateur modifier le nom du modèle. Que diriez-vous quelque chose comme ça? ATTENTION: je n'ai pas testé ce code

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

C’est un exemple très basique, mais j’espère que l’idée sera transmise. Vous pouvez modifier plusieurs autres éléments de la réponse, tels que l'ajout d'informations au contexte du modèle. Vous pouvez utiliser ces variables de contexte pour intégrer l'analyse de votre site, telle que Google Analytics, par exemple.

En prime, vous pourriez refactoriser ce décorateur à l'avenir si vous décidiez de ne plus utiliser les paramètres GET et de passer à quelque chose basé sur les cookies, etc.

Mise à jour Si vous avez déjà beaucoup de vues écrites et que vous ne voulez pas toutes les modifier, vous pouvez écrire votre propre version de 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})
7
Justin Voss

Un code basé sur celui de 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

exemple de fonction utilisant le code:

@ab_test()
def index1(request):
    return (request,'website/index.html', locals())

@ab_test('?ui=33')
def index2(request):
    return (request,'website/index.html', locals())

Que se passe-t-il ici: 1. Le paramètre UI passé est stocké dans la variable de session 2. Le même modèle est chargé à chaque fois, mais une variable de contexte {{ui}} enregistre l'identifiant de l'interface utilisateur (vous pouvez l'utiliser pour modifier le modèle) 3. Si l'utilisateur entre la page sans? Ui = xx, s'il est index2, il est redirigé vers '? Ui = 33', dans le cas d'index1, la variable UI est définie sur 0.

J'utilise 3 pour rediriger de la page principale vers Google Website Optimizer qui, à son tour, redirige vers la page principale avec un paramètre? Ui approprié.

1
kolinko

Ces réponses semblent périmées. De nos jours, Google Analytics est probablement l'option la plus populaire et la plus gratuite pour la plupart des sites. Voici quelques ressources pour intégrer Django à Google Analytics:

Plugins :

Comment Tos :

1
Brian

Django-lean a l'air génial. Je vais essayer de comprendre encore. J'ai fini par lancer ma propre solution, ce qui est suffisant pour ce que j'essayais de faire. J'ai essayé de bien emballer le contenu et de le rendre facile à utiliser pour les débutants. C'est super basique, essayez-le:

https://github.com/crobertsbmw/RobertsAB

1
Chase Roberts

La réponse de Justin est juste ... Je vous recommande de voter pour celui-là, comme il était le premier. Son approche est particulièrement utile si vous avez plusieurs vues nécessitant cet ajustement A/B.

Notez cependant que vous n'avez pas besoin d'un décorateur, ni de modifications dans urls.py, si vous n'avez qu'une poignée de vues. Si vous avez laissé votre fichier urls.py tel quel ...

(r'^foo/', my.view.here),

... vous pouvez utiliser request.GET pour déterminer la variante de vue demandée:

def here(request):
    variant = request.GET.get('ui', some_default)

Si vous souhaitez éviter les noms de modèle de codage en dur pour les vues individuelles A/B/C/etc, faites-leur simplement une convention dans votre schéma de dénomination de modèle (comme le recommande également l'approche de 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')
1
Jarret Hardie

J'ai écrit un exemple de test partagé pour Django que toute personne intéressée à cela pourrait trouver utile -

https://github.com/DanAncona/Django-mini-lean

J'adore entendre ce que vous pensez de cela et comment je peux le rendre plus utile!

0
Dan Ancona