django testing ab-testing

¿Alguna idea sobre las pruebas A/B en el proyecto basado en Django?



testing ab-testing (8)

Acabamos de comenzar a hacer las pruebas A / B para nuestro proyecto basado en Django. ¿Puedo obtener información sobre las mejores prácticas o información útil sobre esta prueba A / B?

Idealmente, cada nueva página de prueba se diferenciará con un único parámetro (como Gmail). mysite.com/?ui=2 debería dar una página diferente. Entonces, para cada vista necesito escribir un decorador para cargar diferentes plantillas basadas en el valor del parámetro ''ui''. Y no quiero codificar los nombres de las plantillas en decoradores. Entonces, ¿cómo sería el patrón urls urls.py?



Django-lean se ve increíble. Voy a tratar de resolverlo todavía. Terminé rodando mi propia solución que es suficiente para lo que estaba tratando de hacer. He intentado empaquetarlo bien y hacerlo fácil de usar para el principiante. Es super básico, pruébalo:

https://github.com/crobertsbmw/RobertsAB


Es útil dar un paso atrás y abstraer lo que las pruebas A / B están tratando de hacer antes de sumergirse en el código. ¿Qué es exactamente lo que necesitamos para realizar una prueba?

  • Un objetivo que tiene una condición
  • Al menos dos Rutas distintas para cumplir la Condición de la Meta
  • Un sistema para enviar espectadores por uno de los Caminos
  • Un sistema para registrar los Resultados de la prueba

Con esto en mente, pensemos en la implementación.

La meta

Cuando pensamos en un objetivo en la web, generalmente nos referimos a que un usuario llega a determinada página o que completa una acción específica, por ejemplo, registrarse exitosamente como usuario o ir a la página de pago.

En Django podríamos modelar eso de varias maneras, quizás ingenuamente dentro de una vista, llamando a una función cada vez que se alcanza un Objetivo:

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

Pero eso no ayuda, porque tendremos que agregar ese código donde sea que lo necesitemos; además, si utilizamos cualquier aplicación conectable, preferimos no editar su código para agregar nuestra prueba A / B.

¿Cómo podemos introducir los Objetivos A / B sin editar directamente el código de vista? ¿Qué tal un Middleware?

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

Eso nos permitiría seguir los Objetivos A / B en cualquier parte del sitio.

¿Cómo sabemos que se han cumplido las condiciones de un objetivo? Para facilitar la implementación, sugeriré que sepamos que un objetivo ha cumplido sus condiciones cuando un usuario llega a una ruta de URL específica. Como beneficio adicional podemos medir esto sin ensuciarnos las manos dentro de una vista. Para volver a nuestro ejemplo de registrar un usuario podríamos decir que este objetivo se cumplió cuando el usuario alcanza la ruta de la URL:

/ registro / completo

Entonces definimos a_b_goal_conditions_met :

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

Caminos

Al pensar en Paths in Django, es natural saltar a la idea de usar diferentes plantillas. Si queda otro camino por explorar. En las pruebas A / B, usted hace pequeñas diferencias entre dos páginas y mide los resultados. Por lo tanto, debe ser una mejor práctica definir una única plantilla de ruta base a partir de la cual deben extenderse todas las rutas a la meta.

¿Cómo debería renderizar estas plantillas? Un decorador es probablemente un buen comienzo. Es una buena práctica en Django incluir un parámetro template_name para sus vistas; un decorador podría alterar este parámetro en tiempo de ejecución.

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

Podrías ver a este decorador introspeccionando la función envuelta y modificando el argumento template_name o buscando las plantillas correctas desde algún lugar (como un Modelo). Si no quisiéramos agregar el decorador a cada función, podríamos implementar esto como parte de nuestro 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

También necesitaríamos agregar alguna forma de mantener un registro de las vistas que tienen pruebas A / B ejecutándose, etc.

Un sistema para enviar visores por un camino

En teoría, esto es fácil, pero hay muchas implementaciones diferentes, por lo que no está claro cuál es el mejor. Sabemos que un buen sistema debe dividir a los usuarios de manera uniforme en el camino. Se debe usar algún método hash. Quizás se pueda usar el módulo del contador de Memcache dividido por el número de Rutas. Tal vez haya una mejor manera.

Un sistema para registrar los resultados de la prueba

Necesitamos registrar cuántos usuarios pasaron por esa ruta; también necesitaremos acceso a esta información cuando el usuario alcance el objetivo (debemos poder decir qué ruta de acceso bajaron para cumplir con la condición de la meta) - Usaremos algún tipo de modelo (s) para registrar los datos y las sesiones de Django o las cookies para conservar la información de ruta hasta que el usuario cumpla con la condición de objetivo.

Pensamientos finales

He dado muchos pseudocódigos para implementar pruebas A / B en Django: lo anterior no es una solución completa, sino un buen comienzo para crear un marco reutilizable para las pruebas A / B en Django.

Como referencia, es posible que desee consultar los Seven Minute A / Bs de Paul Mar en GitHub: ¡es la versión ROR de los anteriores! http://github.com/paulmars/seven_minute_abs/tree/master

Actualizar

En una mayor reflexión e investigación del Optimizador de sitios web de Google, es evidente que hay enormes lagunas en la lógica anterior. Al usar diferentes plantillas para representar Rutas, se rompe todo el almacenamiento en caché en la vista (o si la vista se almacena en caché, ¡siempre servirá la misma ruta!). En lugar de usar Paths, en cambio, robaría la terminología de GWO y usaría la idea de Combinations , es decir, una parte específica de un cambio de plantilla, por ejemplo, cambiar la etiqueta <h1> de un sitio.

La solución incluiría etiquetas de plantilla que se procesarían en JavaScript. Cuando la página se carga en el navegador, JavaScript hace una solicitud a su servidor que busca una de las posibles combinaciones.

¡De esta forma puede probar múltiples combinaciones por página mientras preserva el almacenamiento en caché!

Actualizar

Todavía hay espacio para el cambio de plantilla, por ejemplo, si introduce una página de inicio completamente nueva y desea probar su rendimiento con respecto a la página de inicio anterior, aún desea utilizar la técnica de cambio de plantilla. Lo que hay que tener en cuenta es que tendrá que encontrar la manera de cambiar entre las X versiones de la página en caché. Para hacerlo, debe sobrescribir el middleware en caché estándar para ver si se trata de una prueba A / B que se ejecuta en la URL solicitada. ¡Entonces podría elegir la versión correcta en caché para mostrar!

Actualizar

Usando las ideas descritas anteriormente, he implementado una aplicación enchufable para pruebas básicas A / B Django. Puedes quitártelo de Github:

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



Estas respuestas parecen obsoletas. Hoy en día, Google Analytics es probablemente la opción gratuita más popular y mejor para la mayoría de los sitios. Estos son algunos recursos para integrar django con Google Analytics:

Complementos :

Cómo Tos :


La respuesta de Justin es correcta ... Te recomiendo que votes por esa, como lo fue primero. Su enfoque es particularmente útil si tiene múltiples vistas que necesitan este ajuste A / B.

Sin embargo, tenga en cuenta que no necesita un decorador ni alteraciones en urls.py, si tiene solo un puñado de vistas. Si dejó su archivo urls.py como está ...

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

... puede usar request.GET para determinar la variante de vista solicitada:

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

Si desea evitar los nombres de plantilla de codificación para las vistas individuales A / B / C / etc., simplemente conviértalas en una convención en su esquema de denominación de plantilla (como también recomienda el enfoque 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'')


Si usa los parámetros GET como suggsted ( ?ui=2 Ui ?ui=2 ), entonces no debería tener que tocar urls.py en absoluto. Su decorador puede inspeccionar la request.GET[''ui''] y encuentre lo que necesita.

Para evitar los nombres de las plantillas de codificación, ¿podría ajustar el valor de retorno de la función de vista? En lugar de devolver la salida de render_to_response, puede devolver una tupla de (template_name, context) y dejar que el decorador destruya el nombre de la plantilla. ¿Qué tal algo como esto? ADVERTENCIA: no he probado este código

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

Este es un ejemplo realmente básico, pero espero que transmita la idea. Puede modificar muchas otras cosas sobre la respuesta, como agregar información al contexto de la plantilla. Puede usar esas variables de contexto para integrar con su análisis del sitio, como Google Analytics, por ejemplo.

Como beneficio adicional, podría refactorizar este decorador en el futuro si decide dejar de usar los parámetros GET y pasar a algo basado en cookies, etc.

Actualización Si ya tiene muchas vistas escritas, y no desea modificarlas todas, puede escribir su propia versión 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})


Un código basado en el 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

función de ejemplo usando el código:

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

Qué sucede aquí: 1. El parámetro UI pasado se almacena en la variable de sesión 2. La misma plantilla se carga cada vez, pero una variable de contexto {{ui}} almacena la ID de UI (puede usarla para modificar la plantilla) 3. Si el usuario ingresa a la página sin? Ui = xx, en el caso de index2 se le redirige a ''? Ui = 33'', en el caso de index1, la variable de la interfaz de usuario se establece en 0.

Uso 3 para redirigir desde la página principal al Optimizador de sitios web de Google, que a su vez lo redirecciona a la página principal con un parámetro de? UI apropiado.