Skip to content

Commit

Permalink
Rename treatment to variant
Browse files Browse the repository at this point in the history
  • Loading branch information
kaedroho committed Dec 16, 2020
1 parent 856d51b commit b16b8b9
Show file tree
Hide file tree
Showing 14 changed files with 154 additions and 96 deletions.
28 changes: 28 additions & 0 deletions wagtail_ab_testing/migrations/0010_rename_treatment_to_variant.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 3.1.3 on 2020-12-16 13:40

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('wagtail_ab_testing', '0009_rename_variant_to_version'),
]

operations = [
migrations.RenameField(
model_name='abtest',
old_name='treatment_revision',
new_name='variant_revision',
),
migrations.AlterField(
model_name='abtest',
name='winning_version',
field=models.CharField(choices=[('control', 'Control'), ('variant', 'Variant')], max_length=9, null=True),
),
migrations.AlterField(
model_name='abtesthourlylog',
name='version',
field=models.CharField(choices=[('control', 'Control'), ('variant', 'Variant')], max_length=9),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 3.1.3 on 2020-12-16 13:40

from django.db import migrations, models


def rename_treatment_to_variant_forwards(apps, schema_editor):
AbTest = apps.get_model('wagtail_ab_testing.AbTest')
AbTest.objects.filter(winning_version='treatment').update(winning_version='variant')

AbTestHourlyLog = apps.get_model('wagtail_ab_testing.AbTestHourlyLog')
AbTestHourlyLog.objects.filter(version='treatment').update(version='variant')


def rename_treatment_to_variant_backwards(apps, schema_editor):
AbTest = apps.get_model('wagtail_ab_testing.AbTest')
AbTest.objects.filter(winning_version='variant').update(winning_version='treatment')

AbTestHourlyLog = apps.get_model('wagtail_ab_testing.AbTestHourlyLog')
AbTestHourlyLog.objects.filter(version='variant').update(version='treatment')


class Migration(migrations.Migration):

dependencies = [
('wagtail_ab_testing', '0010_rename_treatment_to_variant'),
]

operations = [
migrations.RunPython(rename_treatment_to_variant_forwards, rename_treatment_to_variant_backwards)
]
60 changes: 30 additions & 30 deletions wagtail_ab_testing/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class AbTest(models.Model):
Represents an A/B test that has been set up by the user.
The live page content is used as the control, the revision pointed to in
the `.treatment_revision` field contains the changes that are being tested.
the `.variant_revision` field contains the changes that are being tested.
"""

class Status(models.TextChoices):
Expand All @@ -38,7 +38,7 @@ class Status(models.TextChoices):

# These two sound similar, but there's a difference:
# 'Finished' means that we've reached the sample size and testing has stopped
# but the user still needs to decide whether to publish the treatment version
# but the user still needs to decide whether to publish the variant version
# or revert back to the control.
# Once they've decided and that action has taken place, the test status is
# updated to 'Completed'.
Expand All @@ -47,18 +47,18 @@ class Status(models.TextChoices):

class Version(models.TextChoices):
CONTROL = 'control', __('Control')
TREATMENT = 'treatment', __('Treatment')
VARIANT = 'variant', __('Variant')

class CompletionAction(models.TextChoices):
# See docstring of the .complete() method for descriptions
DO_NOTHING = 'do-nothing', "Do nothing"
REVERT = 'revert', "Revert to control"
PUBLISH = 'publisn', "Publish treatment"
PUBLISH = 'publisn', "Publish variant"

page = models.ForeignKey('wagtailcore.Page', on_delete=models.CASCADE, related_name='ab_tests')
name = models.CharField(max_length=255)
hypothesis = models.TextField(blank=True)
treatment_revision = models.ForeignKey('wagtailcore.PageRevision', on_delete=models.CASCADE, related_name='+')
variant_revision = models.ForeignKey('wagtailcore.PageRevision', on_delete=models.CASCADE, related_name='+')
goal_event = models.CharField(max_length=255)
goal_page = models.ForeignKey('wagtailcore.Page', null=True, blank=True, on_delete=models.SET_NULL, related_name='+')
sample_size = models.PositiveIntegerField(validators=[MinValueValidator(1)])
Expand Down Expand Up @@ -150,7 +150,7 @@ def finish(self):
Note that this doesn't 'complete' the test: a finished test means
that testing is no longer happening. The test is not complete until
the user decides on the outcome of the test (keep the control or
publish the treatment). This decision is set using the .complete()
publish the variant). This decision is set using the .complete()
method.
"""
self.status = self.Status.FINISHED
Expand All @@ -166,14 +166,14 @@ def complete(self, action, user=None):
Actions can be:
- AbTest.CompletionAction.DO_NOTHING - This just completes
the test but does nothing to the page. The control will
remain the published version and the treatment will be
remain the published version and the variant will be
in draft.
- AbTest.CompletionAction.REVERT - This completes the test
and also creates a new revision to revert the content back
to what it was in the control while the test was taking
place.
- AbTest.CompletionAction.PUBLISH - This completes the test
and also publishes the treatment revision.
and also publishes the variant revision.
"""
self.status = self.Status.COMPLETED
self.save(update_fields=['status'])
Expand All @@ -186,7 +186,7 @@ def complete(self, action, user=None):
self.page.save_revision(user=user, log_action='wagtail.revert').publish(user=user)

elif action == AbTest.CompletionAction.PUBLISH:
self.treatment_revision.publish(user=user)
self.variant_revision.publish(user=user)

def add_participant(self, version=None):
"""
Expand All @@ -195,23 +195,23 @@ def add_participant(self, version=None):
# Get current numbers of participants for each version
stats = self.hourly_logs.aggregate(
control_participants=Sum('participants', filter=Q(version=self.Version.CONTROL)),
treatment_participants=Sum('participants', filter=Q(version=self.Version.TREATMENT)),
variant_participants=Sum('participants', filter=Q(version=self.Version.VARIANT)),
)
control_participants = stats['control_participants'] or 0
treatment_participants = stats['treatment_participants'] or 0
variant_participants = stats['variant_participants'] or 0

# Create an equal number of participants for each version
if version is None:
if treatment_participants > control_participants:
if variant_participants > control_participants:
version = self.Version.CONTROL

elif treatment_participants < control_participants:
version = self.Version.TREATMENT
elif variant_participants < control_participants:
version = self.Version.VARIANT

else:
version = random.choice([
self.Version.CONTROL,
self.Version.TREATMENT,
self.Version.VARIANT,
])

# Add new participant to statistics model
Expand All @@ -222,7 +222,7 @@ def add_participant(self, version=None):
# get a chance to turn into conversions. It's unlikely to make a
# significant difference to the results.
# Note: Adding 1 to account for the new participant
if control_participants + treatment_participants + 1 >= self.sample_size:
if control_participants + variant_participants + 1 >= self.sample_size:
self.finish()

return version
Expand All @@ -240,7 +240,7 @@ def check_for_winner(self):
"""
Performs a Chi-Squared test to check if there is a clear winner.
Returns Version.CONTROL or Version.TREATMENT if there is one. Otherwise, it returns None.
Returns Version.CONTROL or Version.VARIANT if there is one. Otherwise, it returns None.
For more information on what the Chi-Squared test does, see:
https://www.evanmiller.org/ab-testing/chi-squared.html
Expand All @@ -250,30 +250,30 @@ def check_for_winner(self):
stats = self.hourly_logs.aggregate(
control_participants=Sum('participants', filter=Q(version=self.Version.CONTROL)),
control_conversions=Sum('conversions', filter=Q(version=self.Version.CONTROL)),
treatment_participants=Sum('participants', filter=Q(version=self.Version.TREATMENT)),
treatment_conversions=Sum('conversions', filter=Q(version=self.Version.TREATMENT)),
variant_participants=Sum('participants', filter=Q(version=self.Version.VARIANT)),
variant_conversions=Sum('conversions', filter=Q(version=self.Version.VARIANT)),
)
control_participants = stats['control_participants'] or 0
control_conversions = stats['control_conversions'] or 0
treatment_participants = stats['treatment_participants'] or 0
treatment_conversions = stats['treatment_conversions'] or 0
variant_participants = stats['variant_participants'] or 0
variant_conversions = stats['variant_conversions'] or 0

if not control_conversions and not treatment_conversions:
if not control_conversions and not variant_conversions:
return

if control_conversions > control_participants or treatment_conversions > treatment_participants:
if control_conversions > control_participants or variant_conversions > variant_participants:
# Something's up. I'm sure it's already clear in the UI what's going on, so let's not crash
return

# Create a numpy array with values to pass in to Chi-Squared test
control_failures = control_participants - control_conversions
treatment_failures = treatment_participants - treatment_conversions
variant_failures = variant_participants - variant_conversions

if control_failures == 0 and treatment_failures == 0:
if control_failures == 0 and variant_failures == 0:
# Prevent this error: "The internally computed table of expected frequencies has a zero element at (0, 1)."
return

T = np.array([[control_conversions, control_failures], [treatment_conversions, treatment_failures]])
T = np.array([[control_conversions, control_failures], [variant_conversions, variant_failures]])

# Perform Chi-Squared test
p = scipy.stats.chi2_contingency(T, correction=False)[1]
Expand All @@ -283,10 +283,10 @@ def check_for_winner(self):
if 1 - p > required_confidence_level:
# There is a clear winner!
# Return the one with the highest success rate
if (control_conversions / control_participants) > (treatment_conversions / treatment_participants):
if (control_conversions / control_participants) > (variant_conversions / variant_participants):
return self.Version.CONTROL
else:
return self.Version.TREATMENT
return self.Version.VARIANT

def get_status_description(self):
"""
Expand All @@ -303,8 +303,8 @@ def get_status_description(self):
if self.winning_version == AbTest.Version.CONTROL:
return status + " (" + _("Control won") + ")"

elif self.winning_version == AbTest.Version.TREATMENT:
return status + " (" + _("Treatment won") + ")"
elif self.winning_version == AbTest.Version.VARIANT:
return status + " (" + _("Variant won") + ")"

else:
return status + " (" + _("No clear winner") + ")"
Expand Down
18 changes: 9 additions & 9 deletions wagtail_ab_testing/static_src/style/progress.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@import 'vendor/c3.min.css';

$color-control: #0c0073;
$color-treatment: #ef746f;
$color-variant: #ef746f;

$light-teal: #e1f0f0;
$dark-teal: #007273;
Expand Down Expand Up @@ -63,16 +63,16 @@ $charcoal-grey: #333;
}
}

&--treatment {
&--variant {
right: 0;
padding-left: 10px;
color: $color-treatment;
color: $color-variant;

a {
color: $color-treatment !important;
color: $color-variant !important;

&:hover {
color: darken($color-treatment, 10%) !important;
color: darken($color-variant, 10%) !important;
}
}
}
Expand Down Expand Up @@ -105,15 +105,15 @@ $charcoal-grey: #333;
&__version--control &__version-heading {
border-bottom-color: $color-control;
}
&__version--treatment &__version-heading {
border-bottom-color: $color-treatment;
&__version--variant &__version-heading {
border-bottom-color: $color-variant;
}

&__version--control &__version-heading--winner {
background-color: $color-control;
}
&__version--treatment &__version-heading--winner {
background-color: $color-treatment;
&__version--variant &__version-heading--winner {
background-color: $color-variant;
}

&__version-inner {
Expand Down
20 changes: 10 additions & 10 deletions wagtail_ab_testing/templates/wagtail_ab_testing/results.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ <h2>{{ ab_test.name }}</h2>

<a href="{% url 'wagtail_ab_testing:add_test_participants' ab_test.id %}">Add test participants</a>
<a href="{% url 'wagtail_ab_testing:add_test_conversions' ab_test.id 'control' %}">Add conversions for control</a>
<a href="{% url 'wagtail_ab_testing:add_test_conversions' ab_test.id 'treatment' %}">Add conversions for treatment</a>
<a href="{% url 'wagtail_ab_testing:add_test_conversions' ab_test.id 'variant' %}">Add conversions for variant</a>

<div class="abtest-progressbar">
<svg class="abtest-progressbar__sample-size">
Expand Down Expand Up @@ -78,36 +78,36 @@ <h3>{% trans "Control" %} <a href="{% pageurl page %}" target="_blank">{% icon n
{% endif %}
</div>
</div>
<div class="abtest-results__version abtest-results__version--treatment">
<div class="abtest-results__version-heading{% if treatment_is_winner or unclear_winner %} abtest-results__version-heading--winner{% endif %}">
{% if treatment_is_winner %}{% icon "crown" %} {% trans "Winner!" %}{% elif unclear_winner %}{% trans "No clear winner" %}{% endif %}
<div class="abtest-results__version abtest-results__version--variant">
<div class="abtest-results__version-heading{% if variant_is_winner or unclear_winner %} abtest-results__version-heading--winner{% endif %}">
{% if variant_is_winner %}{% icon "crown" %} {% trans "Winner!" %}{% elif unclear_winner %}{% trans "No clear winner" %}{% endif %}
</div>
<div class="abtest-results__version-inner">
<h3>{% trans "Treatment" %} <a href="{% url 'wagtailadmin_pages:view_draft' page.id %}" target="_blank">{% icon name="link-external" %}</a></h3>
<h3>{% trans "Variant" %} <a href="{% url 'wagtailadmin_pages:view_draft' page.id %}" target="_blank">{% icon name="link-external" %}</a></h3>

<ul class="abtest-results__version-stats">
<li>
<div class="abtest-results__version-stat">
{{ treatment_conversions_percent }}%
{{ variant_conversions_percent }}%
</div>
<div class="abtest-results__version-stat-name">
{% trans "Conversion rate" %}
</div>
</li>
<li>
<div class="abtest-results__version-stat">
{{ treatment_conversions }}
{{ variant_conversions }}
</div>
<div class="abtest-results__version-stat-name">
{% trans "Conversions" %} <span>({% blocktrans count treatment_participants as count %}1 user{% plural %}{{ count }} users{% endblocktrans %})</span>
{% trans "Conversions" %} <span>({% blocktrans count variant_participants as count %}1 user{% plural %}{{ count }} users{% endblocktrans %})</span>
</div>
</li>
</ul>

{% if ab_test.status == 'finished' %}
<form method="post">
{% csrf_token %}
<button type="submit" name="action-select-treatment" value="{% trans 'Publish and end test' %}" class="button">{% trans "<span>Publish</span> and end test" %}</button>
<button type="submit" name="action-select-variant" value="{% trans 'Publish and end test' %}" class="button">{% trans "<span>Publish</span> and end test" %}</button>
</form>
{% endif %}
</div>
Expand All @@ -116,7 +116,7 @@ <h3>{% trans "Treatment" %} <a href="{% url 'wagtailadmin_pages:view_draft' page
</div>
{% url 'wagtail_ab_testing:compare_draft' page.id as compare_url%}
{% blocktrans with compare_url=compare_url trimmed %}
<a href="{{ compare_url }}" target="_blank">Compare pages</a> and see how the control and treatment pages differ.
<a href="{{ compare_url }}" target="_blank">Compare pages</a> and see how the control and variant pages differ.
{% endblocktrans %}
<hr>
<h3>{% trans "Conversions over time" %}</h3>
Expand Down
Loading

0 comments on commit b16b8b9

Please sign in to comment.