Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Core] Be pep8 compliant. #173

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 34 additions & 17 deletions experiments/admin.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from django.contrib import admin
from django.contrib.admin.utils import unquote
from django.conf.urls import url
from django import forms
from django.http import JsonResponse, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
from django.http import (
JsonResponse, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden)
from django.utils import timezone

from experiments.admin_utils import get_result_context
from experiments.models import Experiment
from experiments import conf
from django.conf.urls import url

from experiments.utils import participant


Expand Down Expand Up @@ -49,20 +52,25 @@ def get_form(self, request, obj=None, **kwargs):
"""
if obj:
if obj.alternatives:
choices = [(alternative, alternative) for alternative in obj.alternatives.keys()]
choices = [
(alternative, alternative)
for alternative in obj.alternatives.keys()]
else:
choices = [(conf.CONTROL_GROUP, conf.CONTROL_GROUP)]

class ExperimentModelForm(forms.ModelForm):
default_alternative = forms.ChoiceField(choices=choices,
initial=obj.default_alternative,
required=False)
default_alternative = forms.ChoiceField(
choices=choices, initial=obj.default_alternative,
required=False)

kwargs['form'] = ExperimentModelForm
return super(ExperimentAdmin, self).get_form(request, obj=obj, **kwargs)
return super(ExperimentAdmin, self).get_form(
request, obj=obj, **kwargs)

def save_model(self, request, obj, form, change):
if change:
obj.set_default_alternative(form.cleaned_data['default_alternative'])
obj.set_default_alternative(
form.cleaned_data['default_alternative'])
obj.save()

# --------------------------------------- Overriding admin views
Expand Down Expand Up @@ -90,22 +98,29 @@ def _admin_view_context(self, extra_context=None):
return context

def add_view(self, request, form_url='', extra_context=None):
return super(ExperimentAdmin, self).add_view(request,
form_url=form_url,
extra_context=self._admin_view_context(extra_context=extra_context))
return super(ExperimentAdmin, self).add_view(
request, form_url=form_url, extra_context=self._admin_view_context(
extra_context=extra_context))

def change_view(self, request, object_id, form_url='', extra_context=None):
experiment = self.get_object(request, unquote(object_id))
context = self._admin_view_context(extra_context=extra_context)
context.update(get_result_context(request, experiment))
return super(ExperimentAdmin, self).change_view(request, object_id, form_url=form_url, extra_context=context)

return super(ExperimentAdmin, self).change_view(
request, object_id, form_url=form_url, extra_context=context)

# --------------------------------------- Views for ajax functionality

def get_urls(self):
experiment_urls = [
url(r'^set-alternative/$', self.admin_site.admin_view(self.set_alternative_view), name='experiment_admin_set_alternative'),
url(r'^set-state/$', self.admin_site.admin_view(self.set_state_view), name='experiment_admin_set_state'),
url(
r'^set-alternative/$', self.admin_site.admin_view(
self.set_alternative_view),
name='experiment_admin_set_alternative'),
url(
r'^set-state/$', self.admin_site.admin_view(
self.set_state_view), name='experiment_admin_set_state'),
]
return experiment_urls + super(ExperimentAdmin, self).get_urls()

Expand All @@ -124,7 +139,8 @@ def set_alternative_view(self, request):
participant(request).set_alternative(experiment_name, alternative_name)
return JsonResponse({
'success': True,
'alternative': participant(request).get_alternative(experiment_name)
'alternative': participant(
request).get_alternative(experiment_name)
})

def set_state_view(self, request):
Expand All @@ -140,7 +156,8 @@ def set_state_view(self, request):
return HttpResponseBadRequest()

try:
experiment = Experiment.objects.get(name=request.POST.get("experiment"))
experiment = Experiment.objects.get(
name=request.POST.get("experiment"))
except Experiment.DoesNotExist:
return HttpResponseBadRequest()

Expand All @@ -155,5 +172,5 @@ def set_state_view(self, request):

return HttpResponse()

admin.site.register(Experiment, ExperimentAdmin)

admin.site.register(Experiment, ExperimentAdmin)
99 changes: 72 additions & 27 deletions experiments/admin_utils.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import json

from experiments.experiment_counters import ExperimentCounter
from experiments.significance import chi_square_p_value, mann_whitney
from experiments.utils import participant
from experiments import conf

import json


MIN_ACTIONS_TO_SHOW = 3


def rate(a, b):
if not b or a == None:
if not b or a is None:
return None
return 100. * a / b

Expand All @@ -35,9 +35,11 @@ def chi_squared_confidence(a_count, a_conversion, b_count, b_conversion):
def average_actions(distribution):
total_users = 0
total_actions = 0

for actions, frequency in distribution.items():
total_users += frequency
total_actions += actions * frequency

if total_users:
return total_actions / float(total_users)
else:
Expand All @@ -52,6 +54,7 @@ def fixup_distribution(distribution, count):

def mann_whitney_confidence(a_distribution, b_distribution):
p_value = mann_whitney(a_distribution, b_distribution)[1]

if p_value is not None:
return (1 - p_value * 2) * 100 # Two tailed probability
else:
Expand All @@ -60,15 +63,17 @@ def mann_whitney_confidence(a_distribution, b_distribution):

def points_with_surrounding_gaps(points):
"""
This function makes sure that any gaps in the sequence provided have stopper points at their beginning
and end so a graph will be drawn with correct 0 ranges. This is more efficient than filling in all points
up to the maximum value. For example:
This function makes sure that any gaps in the sequence provided have
stopper points at their beginning and end so a graph will be drawn with
correct 0 ranges. This is more efficient than filling in all points up to
the maximum value. For example:

input: [1,2,3,10,11,13]
output [1,2,3,4,9,10,11,12,13]
"""
points_with_gaps = []
last_point = -1

for point in points:
if last_point + 1 == point:
pass
Expand All @@ -77,31 +82,46 @@ def points_with_surrounding_gaps(points):
else:
points_with_gaps.append(last_point + 1)
points_with_gaps.append(point - 1)

points_with_gaps.append(point)
last_point = point

return points_with_gaps


def conversion_distributions_to_graph_table(conversion_distributions):
ordered_distributions = list(conversion_distributions.items())
total_entries = dict((name, float(sum(dist.values()) or 1)) for name, dist in ordered_distributions)
total_entries = dict((
name, float(sum(dist.values()) or 1))
for name, dist in ordered_distributions)
graph_head = [['x'] + [name for name, dist in ordered_distributions]]

points_in_any_distribution = sorted(set(k for name, dist in ordered_distributions for k in dist.keys()))
points_in_any_distribution = sorted(
set(k for name, dist in ordered_distributions for k in dist.keys()))
points_with_gaps = points_with_surrounding_gaps(points_in_any_distribution)
graph_body = [[point] + [dist.get(point, 0) / total_entries[name] for name, dist in ordered_distributions] for point in points_with_gaps]
graph_body = [
[point] + [dist.get(point, 0) / total_entries[
name] for name, dist in ordered_distributions]
for point in points_with_gaps]

accumulator = [0] * len(ordered_distributions)
for point in range(len(graph_body) - 1, -1, -1):
accumulator = [graph_body[point][j + 1] + accumulator[j] for j in range(len(ordered_distributions))]
accumulator = [
graph_body[point][j + 1] + accumulator[j] for j in range(len(
ordered_distributions))]
graph_body[point][1:] = accumulator

interesting_points = [point for point in points_in_any_distribution if max(dist.get(point, 0) for name, dist in ordered_distributions) >= MIN_ACTIONS_TO_SHOW]
interesting_points = [
point for point in points_in_any_distribution if max(
dist.get(point, 0)
for name, dist in ordered_distributions) >= MIN_ACTIONS_TO_SHOW]
if len(interesting_points):
highest_interesting_point = max(interesting_points)
else:
highest_interesting_point = 0
graph_body = [g for g in graph_body if g[0] <= highest_interesting_point and g[0] != 0]
graph_body = [
g for g in graph_body if g[0] <= highest_interesting_point
and g[0] != 0]

graph_table = graph_head + graph_body
return json.dumps(graph_table)
Expand All @@ -121,46 +141,69 @@ def get_result_context(request, experiment):
relevant_goals = set(chi2_goals + mwu_goals)

alternatives = {}

for alternative_name in experiment.alternatives.keys():
alternatives[alternative_name] = experiment_counter.participant_count(experiment, alternative_name)
alternatives[alternative_name] = experiment_counter.participant_count(
experiment, alternative_name)

alternatives = sorted(alternatives.items())

control_participants = experiment_counter.participant_count(experiment, conf.CONTROL_GROUP)
control_participants = experiment_counter.participant_count(
experiment, conf.CONTROL_GROUP)

results = {}

for goal in conf.ALL_GOALS:
show_mwu = goal in mwu_goals

alternatives_conversions = {}
control_conversions = experiment_counter.goal_count(experiment, conf.CONTROL_GROUP, goal)
control_conversion_rate = rate(control_conversions, control_participants)
control_conversions = experiment_counter.goal_count(
experiment, conf.CONTROL_GROUP, goal)
control_conversion_rate = rate(
control_conversions, control_participants)

if show_mwu:
mwu_histogram = {}
control_conversion_distribution = fixup_distribution(experiment_counter.goal_distribution(experiment, conf.CONTROL_GROUP, goal), control_participants)
control_average_goal_actions = average_actions(control_conversion_distribution)
control_conversion_distribution = fixup_distribution(
experiment_counter.goal_distribution(
experiment, conf.CONTROL_GROUP, goal),
control_participants)
control_average_goal_actions = average_actions(
control_conversion_distribution)
mwu_histogram['control'] = control_conversion_distribution
else:
control_average_goal_actions = None

for alternative_name in experiment.alternatives.keys():
if not alternative_name == conf.CONTROL_GROUP:
alternative_conversions = experiment_counter.goal_count(experiment, alternative_name, goal)
alternative_participants = experiment_counter.participant_count(experiment, alternative_name)
alternative_conversion_rate = rate(alternative_conversions, alternative_participants)
alternative_confidence = chi_squared_confidence(alternative_participants, alternative_conversions, control_participants, control_conversions)
alternative_conversions = experiment_counter.goal_count(
experiment, alternative_name, goal)
alternative_participants = experiment_counter\
.participant_count(experiment, alternative_name)
alternative_conversion_rate = rate(
alternative_conversions, alternative_participants)
alternative_confidence = chi_squared_confidence(
alternative_participants, alternative_conversions,
control_participants, control_conversions)

if show_mwu:
alternative_conversion_distribution = fixup_distribution(experiment_counter.goal_distribution(experiment, alternative_name, goal), alternative_participants)
alternative_average_goal_actions = average_actions(alternative_conversion_distribution)
alternative_conversion_distribution = fixup_distribution(
experiment_counter.goal_distribution(
experiment, alternative_name, goal),
alternative_participants)
alternative_average_goal_actions = average_actions(
alternative_conversion_distribution)
alternative_distribution_confidence = mann_whitney_confidence(alternative_conversion_distribution, control_conversion_distribution)
mwu_histogram[alternative_name] = alternative_conversion_distribution
else:
alternative_average_goal_actions = None
alternative_distribution_confidence = None

alternative = {
'conversions': alternative_conversions,
'conversion_rate': alternative_conversion_rate,
'improvement': improvement(alternative_conversion_rate, control_conversion_rate),
'improvement': improvement(
alternative_conversion_rate, control_conversion_rate),
'confidence': alternative_confidence,
'average_goal_actions': alternative_average_goal_actions,
'mann_whitney_confidence': alternative_distribution_confidence,
Expand All @@ -178,7 +221,8 @@ def get_result_context(request, experiment):
"alternatives": sorted(alternatives_conversions.items()),
"relevant": goal in relevant_goals or relevant_goals == {u''},
"mwu": goal in mwu_goals,
"mwu_histogram": conversion_distributions_to_graph_table(mwu_histogram) if show_mwu else None
"mwu_histogram": conversion_distributions_to_graph_table(
mwu_histogram) if show_mwu else None
}

return {
Expand All @@ -187,5 +231,6 @@ def get_result_context(request, experiment):
'control_participants': control_participants,
'results': results,
'column_count': len(alternatives_conversions) * 3 + 2, # Horrible coupling with template design
'user_alternative': participant(request).get_alternative(experiment.name),
'user_alternative': participant(
request).get_alternative(experiment.name),
}
10 changes: 7 additions & 3 deletions experiments/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ class ExperimentsConfig(AppConfig):

def ready(self):
from django.contrib.auth.signals import user_logged_in, user_logged_out
from experiments.signal_handlers import transfer_enrollments_to_user, handle_user_logged_out
from experiments.signal_handlers import (
transfer_enrollments_to_user, handle_user_logged_out)

user_logged_in.connect(transfer_enrollments_to_user, dispatch_uid="experiments_user_logged_in")
user_logged_out.connect(handle_user_logged_out, dispatch_uid="experiments_user_logged_out")
user_logged_in.connect(
transfer_enrollments_to_user,
dispatch_uid="experiments_user_logged_in")
user_logged_out.connect(
handle_user_logged_out, dispatch_uid="experiments_user_logged_out")
9 changes: 6 additions & 3 deletions experiments/conf.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.conf import settings
from itertools import chain
import re
from itertools import chain

from django.conf import settings

CONTROL_GROUP = 'control'

Expand All @@ -21,6 +22,8 @@

CONFIRM_HUMAN = getattr(settings, 'EXPERIMENTS_CONFIRM_HUMAN', True)

CONFIRM_HUMAN_SESSION_KEY = getattr(settings, 'EXPERIMENTS_CONFIRM_HUMAN_SESSION_KEY', 'experiments_verified_human')
CONFIRM_HUMAN_SESSION_KEY = getattr(
settings, 'EXPERIMENTS_CONFIRM_HUMAN_SESSION_KEY',
'experiments_verified_human')

BOT_REGEX = re.compile("(Baidu|Gigabot|Googlebot|YandexBot|AhrefsBot|TVersity|libwww-perl|Yeti|lwp-trivial|msnbot|bingbot|facebookexternalhit|Twitterbot|Twitmunin|SiteUptime|TwitterFeed|Slurp|WordPress|ZIBB|ZyBorg)", re.IGNORECASE)
Loading