61 Stimmen

Irgendwelche Gedanken über A/B-Tests in Django-basierten Projekt?

Wir haben gerade damit begonnen, die A/B-Tests für unser Django-basiertes Projekt durchzuführen. Kann ich einige Informationen über Best Practices oder nützliche Erkenntnisse über diese A/B-Tests erhalten.

Idealerweise wird jede neue Testseite mit einem einzigen Parameter unterschieden (genau wie Gmail). mysite.com/?ui=2 sollte eine andere Seite ergeben. Also muss ich für jede Ansicht einen Decorator schreiben, um verschiedene Templates basierend auf dem 'ui' Parameterwert zu laden. Und ich möchte keine Template-Namen in Dekoratoren fest codieren. Also wie würde urls.py url Muster sein?

106voto

jb. Punkte 9827

Es ist sinnvoll, einen Schritt zurückzutreten und zu abstrahieren, was mit A/B-Tests erreicht werden soll, bevor man in den Code eintaucht. Was genau brauchen wir, um einen Test durchzuführen?

  • Ein Ziel, das eine Bedingung hat
  • Mindestens zwei verschiedene Wege zur Erfüllung der Zielbedingung
  • Ein System, das die Zuschauer auf einen der Pfade schickt
  • Ein System zur Aufzeichnung der Ergebnisse der Prüfung

In diesem Sinne sollten wir über die Umsetzung nachdenken.

Die Zielsetzung

Wenn wir im Web von einem Ziel sprechen, meinen wir in der Regel, dass ein Nutzer eine bestimmte Seite erreicht oder eine bestimmte Aktion durchführt, z. B. sich erfolgreich als Nutzer registriert oder zur Kasse geht.

In Django können wir das auf verschiedene Arten modellieren - vielleicht naiv innerhalb einer Ansicht, indem wir eine Funktion aufrufen, wenn ein Ziel erreicht wurde:

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

Aber das hilft uns nicht weiter, weil wir den Code überall dort hinzufügen müssen, wo wir ihn brauchen - und wenn wir irgendwelche pluggable Anwendungen verwenden, möchten wir deren Code nicht bearbeiten, um unseren A/B-Test hinzuzufügen.

Wie können wir A/B-Ziele einführen, ohne den Code der Ansicht direkt zu bearbeiten? Was ist mit einer Middleware?

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

Das würde uns ermöglichen, A/B-Ziele überall auf der Website zu verfolgen.

Woher wissen wir, dass die Bedingungen eines Ziels erfüllt sind? Der Einfachheit halber schlage ich vor, dass wir wissen, dass die Bedingungen eines Ziels erfüllt sind, wenn ein Benutzer einen bestimmten URL-Pfad erreicht. Als Bonus können wir dies messen, ohne uns die Hände in einer Ansicht schmutzig zu machen. Um auf unser Beispiel der Registrierung eines Benutzers zurückzukommen, könnten wir sagen, dass dieses Ziel erfüllt ist, wenn der Benutzer den URL-Pfad erreicht:

/registrierung/vollständig

Wir definieren also a_b_goal_conditions_met :

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

Pfade

Wenn man über Pfade in Django nachdenkt, ist es naheliegend, auf die Idee zu kommen, verschiedene Vorlagen zu verwenden. Ob es einen anderen Weg gibt, muss noch erforscht werden. Bei A/B-Tests macht man kleine Unterschiede zwischen zwei Seiten und misst die Ergebnisse. Daher sollte es eine Best Practice sein, ein einziges Basis-Path-Template zu definieren, von dem alle Paths zum Goal ausgehen sollten.

Wie sollen diese Vorlagen dargestellt werden? Ein Dekorator ist wahrscheinlich ein guter Anfang - es ist eine bewährte Praxis in Django, einen Parameter einzuschließen template_name zu Ihren Ansichten kann ein Dekorator diesen Parameter zur Laufzeit ändern.

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

Man könnte sehen, dass dieser Dekorator entweder die umhüllte Funktion introspektiert und die template_name oder die richtigen Vorlagen von irgendwoher (z. B. aus einem Modell) nachschlagen. Wenn wir den Dekorator nicht zu jeder Funktion hinzufügen wollten, könnten wir dies als Teil unserer ABMiddleware implementieren:

    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

Wir müssten auch eine Möglichkeit hinzufügen, um zu verfolgen, in welchen Ansichten A/B-Tests laufen usw.

Ein System zum Senden von Zuschauern auf einen Pfad

Theoretisch ist dies einfach, aber es gibt viele verschiedene Implementierungen, so dass es nicht klar ist, welche die beste ist. Wir wissen, dass ein gutes System die Benutzer gleichmäßig auf die Pfade aufteilen sollte - es muss eine Hash-Methode verwendet werden - vielleicht könnte man den Modulus des Memcache-Zählers geteilt durch die Anzahl der Pfade verwenden - vielleicht gibt es einen besseren Weg.

Ein System zur Aufzeichnung der Testergebnisse

Wir müssen aufzeichnen, wie viele Benutzer ging, was Pfad - wir brauchen auch den Zugriff auf diese Informationen, wenn der Benutzer das Ziel erreicht (wir müssen in der Lage sein, zu sagen, welche Pfad sie kamen, um die Bedingung des Ziels erfüllt) - wir verwenden eine Art von Model (s), um die Daten und entweder Django Sessions oder Cookies, um die Pfad-Informationen zu persistieren, bis der Benutzer die Goal-Bedingung erfüllt.

Abschließende Überlegungen

Ich habe eine Menge Pseudo-Code für die Implementierung von A/B-Tests in Django gegeben - das oben genannte ist keineswegs eine vollständige Lösung, sondern ein guter Anfang zur Schaffung eines wiederverwendbaren Frameworks für A/B-Tests in Django.

Als Referenz können Sie sich Paul Mar's Seven Minute A/Bs auf GitHub ansehen - es ist die ROR-Version des oben genannten! http://github.com/paulmars/seven_minute_abs/tree/master


Update

Bei näherer Betrachtung und Untersuchung des Google Website Optimizer wird deutlich, dass die obige Logik klaffende Löcher aufweist. Durch die Verwendung von verschiedenen Vorlagen, um Pfade darzustellen, brechen Sie alle Zwischenspeicherung auf der Ansicht (oder wenn die Ansicht zwischengespeichert wird, wird es immer den gleichen Pfad dienen!). Statt der Verwendung von Pfaden würde ich stattdessen GWO-Terminologie stehlen und die Idee von Combinations - d.h. ein bestimmter Teil einer Vorlage wird geändert - z.B. wird die <h1> Tag einer Website.

Die Lösung wäre die Verwendung von Template-Tags, die in JavaScript umgewandelt würden. Wenn die Seite im Browser geladen wird, stellt das JavaScript eine Anfrage an Ihren Server, der eine der möglichen Kombinationen abruft.

Auf diese Weise können Sie mehrere Kombinationen pro Seite testen und gleichzeitig das Caching beibehalten!


Update

Es gibt immer noch Raum für Template-Switching - sagen wir zum Beispiel, Sie führen eine völlig neue Homepage ein und wollen deren Leistung im Vergleich zur alten Homepage testen - Sie würden immer noch die Template-Switching-Technik verwenden wollen. Dabei müssen Sie sich allerdings überlegen, wie Sie zwischen einer bestimmten Anzahl von zwischengespeicherten Versionen der Seite umschalten können. Um dies zu tun, müssten Sie die Standard-Cache-Middleware außer Kraft setzen, um zu sehen, ob ein A/B-Test auf der angeforderten URL läuft. Dann könnte es die richtige gecachte Version zum Anzeigen auswählen!!!


Update

Unter Verwendung der oben beschriebenen Ideen habe ich eine pluggable App für grundlegende A/B-Tests Django implementiert. Sie können es von Github bekommen:

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

0 Stimmen

Danke Jb, ich weiß deine Lösung wirklich zu schätzen. Ich werde versuchen, sie zu implementieren, und Ihnen dann mein Feedback dazu geben.

8 Stimmen

Dies ist einer der besten Beiträge, die ich in dem Jahr, in dem ich auf dieser Website bin, gesehen habe. Großartiges Zeug.

3 Stimmen

Heilige Handgranate von Antiochia, du hörst nicht auf, bis sie fertig ist.

8voto

Justin Voss Punkte 6174

Wenn Sie die GET-Parameter verwenden, wie Sie es vorgeschlagen haben ( ?ui=2 ), dann sollten Sie urls.py überhaupt nicht mehr anfassen müssen. Ihr Dekorateur kann inspizieren request.GET['ui'] und finden, was es braucht.

Um das Festcodieren von Vorlagennamen zu vermeiden, könnten Sie vielleicht den Rückgabewert der Ansichtsfunktion einschließen? Anstatt die Ausgabe von render_to_response zurückzugeben, könnten Sie ein Tupel von (template_name, context) und lassen Sie den Dekorator den Namen der Vorlage ändern. Wie wäre es mit etwas wie diesem? WARNUNG: Ich habe diesen Code nicht getestet

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

Dies ist ein sehr einfaches Beispiel, aber ich hoffe, dass es die Idee gut vermittelt. Sie könnten noch verschiedene andere Dinge an der Antwort ändern, z. B. Informationen zum Kontext der Vorlage hinzufügen. Sie könnten diese Kontextvariablen verwenden, um sie in Ihre Website-Analysen zu integrieren, wie z. B. Google Analytics.

Als Bonus können Sie diesen Dekorator in der Zukunft refaktorisieren, wenn Sie sich entscheiden, keine GET-Parameter mehr zu verwenden und zu etwas überzugehen, das auf Cookies etc. basiert.

Update Wenn Sie bereits viele Ansichten geschrieben haben und diese nicht alle ändern wollen, können Sie Ihre eigene Version von 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})

0 Stimmen

Aber dazu müssen alle bisherigen Ansichten geändert werden, oder? Das möchte ich nicht tun. Gibt es einen alternativen Weg, so dass mein alter Code bleibt, wie es und diese ab-Testing steht oben auf, dass?

0 Stimmen

Sie könnten Ihre eigene Version von render_to_response schreiben, nehme ich an, die mit diesem Dekorator interagieren würde, um das zu erreichen, was Sie wollen. Ich werde meiner Antwort ein Codebeispiel hinzufügen, um es zu demonstrieren.

0 Stimmen

Justin: Ihre beiden Vorschläge sind ausgezeichnet. Maddy: Wenn Sie eine ganze Website einem A/B-Test unterziehen wollen, dann müssen Sie irgendwo in Ihrer Anfrage-/Antwortkette in den sauren Apfel beißen und programmatisch oder deklarativ angeben, welche Vorlagen mit welchen "UI"-Nummern übereinstimmen.

1voto

Jarret Hardie Punkte 89900

Justins Antwort ist richtig... Ich empfehle Ihnen, für ihn zu stimmen, da er der Erste war. Sein Ansatz ist besonders nützlich, wenn Sie mehrere Ansichten haben, die diese A/B-Anpassung benötigen.

Beachten Sie jedoch, dass Sie keinen Decorator oder Änderungen an urls.py benötigen, wenn Sie nur eine Handvoll Ansichten haben. Wenn Sie Ihre urls.py Datei so lassen wie sie ist...

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

... können Sie request.GET verwenden, um die angeforderte Ansichtsvariante zu ermitteln:

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

Wenn Sie die Hardcodierung von Vorlagennamen für die einzelnen A/B/C/etc-Ansichten vermeiden möchten, machen Sie sie einfach zu einer Konvention in Ihrem Vorlagenbenennungsschema (wie es auch Justins Ansatz empfiehlt):

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')

0 Stimmen

Das würde funktionieren, aber ich denke, wenn Sie mehr als eine Ansicht testen wollen, dann würde das Verschieben dieses Codes in einen Dekorator eine Menge Tipparbeit sparen.

0 Stimmen

Ja, ich stimme zu, Justin. Ich bearbeite meine Antwort, weil wir uns so ähnlich sind, aber deine ist früher.

0 Stimmen

Das legt nahe, dass ich für jede Ansicht einen eigenen Dekorator schreiben muss. Das will ich aber nicht. Wenn ui=2 auf der gesamten Website erwähnt wird (d.h. jede URL, die mit ?ui=2 endet), lädt es Version 2 der gesamten Website. Es wird also ein generischer Dekorator benötigt, der Version 2 Templates über jede URL lädt. Habe ich mich klar ausgedrückt? Das dynamische Ändern von Templates (mit ui=3, ui=3) für jede Ansicht sollte auf ähnliche Weise geschehen. Also sollte ein Dekorator ausreichen. Ist das möglich?

1voto

kolinko Punkte 1623

Ein Code, der auf dem Code von Justin Voss basiert:

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

Beispielfunktion unter Verwendung des Codes:

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

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

Was hier passiert: 1. Der übergebene UI-Parameter wird in der Session-Variablen gespeichert 2. Die gleiche Vorlage wird jedes Mal geladen, aber eine Kontextvariable {{ui}} speichert die UI-ID (Sie können sie verwenden, um die Vorlage zu ändern) 3. Wenn der Benutzer die Seite ohne ?ui=xx aufruft, wird er im Falle von index2 zu '?ui=33' weitergeleitet, im Falle von index1 wird die UI-Variable auf 0 gesetzt.

Ich verwende 3, um von der Hauptseite auf Google Website Optimizer umzuleiten, der wiederum mit einem geeigneten ?ui-Parameter zurück auf die Hauptseite umleitet.

0voto

Adin Punkte 1

Sie können auch A/B-Tests mit Google Optimize durchführen. Dazu müssen Sie Ihrer Website Google Analytics hinzufügen. Wenn Sie dann ein Google Optimize-Experiment erstellen, erhält jeder Nutzer ein Cookie mit einer anderen Versuchsvariante (entsprechend der Gewichtung für jede Variante). Sie können dann die Variante aus dem Cookie extrahieren und verschiedene Versionen Ihrer Anwendung anzeigen. Sie können das folgende Snippet verwenden, um die Variante zu extrahieren:

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

Es gibt jedoch ein Django-Paket, das sich gut mit Google Optimize integrieren lässt: https://github.com/adinhodovic/django-google-optimize/ .

Und hier ist ein Blog-Beitrag über die Verwendung des Pakets und wie Google Optimize funktioniert: https://hodovi.cc/blog/django-b-testing-google-optimize/ .

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X