HardWorkingStation commited on
Commit
853a5d2
·
0 Parent(s):

Initial commit

Browse files
.gitattributes ADDED
@@ -0,0 +1 @@
 
 
1
+ data/* filter=lfs diff=lfs merge=lfs -text
.github/workflow/main.yaml ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Sync to HuggingFace hub
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+ branches: [main]
7
+ # to run this workflow manually from the Actions tab
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+
12
+ check_files:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - name: Check large files
16
+ uses: ActionsDesk/[email protected]
17
+ with:
18
+ filesizelimit: 10485760 # this is 10MB so we can sync to HF Spaces
19
+
20
+ sync-to-hub:
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - uses: actions/checkout@v2
24
+ with:
25
+ fetch-depth: 0
26
+ - name: Push to hub
27
+ env:
28
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
29
+ run: git push --force https://HF_USERNAME:[email protected]/spaces/versus666/ABTest_Lab main
30
+ needs: check_files
.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ test
README.md ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: A/B Lab
3
+ emoji: 🚀 🚀 🚀
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: streamlit
7
+ sdk_version: 1.10.0
8
+ app_file: src/app.py
9
+ pinned: false
10
+ ---
data/ab_test.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:573480e296cd1695033e00db0d31c4293b0b048db92a91f1a7a49358bb6711eb
3
+ size 25030766
images/ab-duration.png ADDED
images/ab-structure.png ADDED
images/hypotesis.png ADDED
images/main.jpg ADDED
images/peeking_problem.png ADDED
requirements.txt ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ altair==4.2.0
2
+ argon2-cffi==21.3.0
3
+ argon2-cffi-bindings==21.2.0
4
+ asttokens==2.0.5
5
+ attrs==21.4.0
6
+ backcall==0.2.0
7
+ beautifulsoup4==4.11.1
8
+ bleach==5.0.1
9
+ blinker==1.5
10
+ cachetools==5.2.0
11
+ certifi==2022.6.15
12
+ cffi==1.15.1
13
+ charset-normalizer==2.1.0
14
+ click==8.1.3
15
+ colorama==0.4.5
16
+ commonmark==0.9.1
17
+ debugpy==1.6.2
18
+ decorator==5.1.1
19
+ defusedxml==0.7.1
20
+ entrypoints==0.4
21
+ executing==0.9.1
22
+ fastjsonschema==2.16.1
23
+ gitdb==4.0.9
24
+ GitPython==3.1.27
25
+ idna==3.3
26
+ importlib-metadata==4.12.0
27
+ ipykernel==6.15.1
28
+ ipython==8.4.0
29
+ ipython-genutils==0.2.0
30
+ ipywidgets==7.7.1
31
+ jedi==0.18.1
32
+ Jinja2==3.1.2
33
+ jsonschema==4.7.2
34
+ jupyter==1.0.0
35
+ jupyter-client==7.3.4
36
+ jupyter-console==6.4.4
37
+ jupyter-core==4.11.1
38
+ jupyterlab-pygments==0.2.2
39
+ jupyterlab-widgets==1.1.1
40
+ MarkupSafe==2.1.1
41
+ matplotlib-inline==0.1.3
42
+ mistune==0.8.4
43
+ nbclient==0.6.6
44
+ nbconvert==6.5.0
45
+ nbformat==5.4.0
46
+ nest-asyncio==1.5.5
47
+ notebook==6.4.12
48
+ numpy==1.23.1
49
+ packaging==21.3
50
+ pandas==1.4.3
51
+ pandocfilters==1.5.0
52
+ parso==0.8.3
53
+ patsy==0.5.2
54
+ pickleshare==0.7.5
55
+ Pillow==9.2.0
56
+ plotly==5.9.0
57
+ prometheus-client==0.14.1
58
+ prompt-toolkit==3.0.30
59
+ protobuf==3.20.1
60
+ psutil==5.9.1
61
+ pure-eval==0.2.2
62
+ pyarrow==8.0.0
63
+ pycparser==2.21
64
+ pydeck==0.7.1
65
+ Pygments==2.12.0
66
+ Pympler==1.0.1
67
+ pyparsing==3.0.9
68
+ pyrsistent==0.18.1
69
+ python-dateutil==2.8.2
70
+ pytz==2022.1
71
+ pytz-deprecation-shim==0.1.0.post0
72
+ pyzmq==23.2.0
73
+ qtconsole==5.3.1
74
+ QtPy==2.1.0
75
+ requests==2.28.1
76
+ rich==12.5.1
77
+ scipy==1.8.1
78
+ semver==2.13.0
79
+ Send2Trash==1.8.0
80
+ six==1.16.0
81
+ smmap==5.0.0
82
+ soupsieve==2.3.2.post1
83
+ stack-data==0.3.0
84
+ statsmodels==0.13.2
85
+ streamlit==1.11.0
86
+ tenacity==8.0.1
87
+ terminado==0.15.0
88
+ tinycss2==1.1.1
89
+ toml==0.10.2
90
+ toolz==0.12.0
91
+ tornado==6.2
92
+ traitlets==5.3.0
93
+ typing_extensions==4.3.0
94
+ tzdata==2022.1
95
+ tzlocal==4.2
96
+ urllib3==1.26.11
97
+ validators==0.20.0
98
+ watchdog==2.1.9
99
+ wcwidth==0.2.5
100
+ webencodings==0.5.1
101
+ widgetsnbextension==3.6.1
102
+ zipp==3.8.1
103
+ altair==4.2.0
104
+ argon2-cffi==21.3.0
105
+ argon2-cffi-bindings==21.2.0
106
+ asttokens==2.0.5
107
+ attrs==21.4.0
108
+ backcall==0.2.0
109
+ beautifulsoup4==4.11.1
110
+ bleach==5.0.1
111
+ blinker==1.5
112
+ cachetools==5.2.0
113
+ certifi==2022.6.15
114
+ cffi==1.15.1
115
+ charset-normalizer==2.1.0
116
+ click==8.1.3
117
+ colorama==0.4.5
118
+ commonmark==0.9.1
119
+ debugpy==1.6.2
120
+ decorator==5.1.1
121
+ defusedxml==0.7.1
122
+ entrypoints==0.4
123
+ executing==0.9.1
124
+ fastjsonschema==2.16.1
125
+ gitdb==4.0.9
126
+ GitPython==3.1.27
127
+ idna==3.3
128
+ importlib-metadata==4.12.0
129
+ ipykernel==6.15.1
130
+ ipython==8.4.0
131
+ ipython-genutils==0.2.0
132
+ ipywidgets==7.7.1
133
+ jedi==0.18.1
134
+ Jinja2==3.1.2
135
+ jsonschema==4.7.2
136
+ jupyter==1.0.0
137
+ jupyter-client==7.3.4
138
+ jupyter-console==6.4.4
139
+ jupyter-core==4.11.1
140
+ jupyterlab-pygments==0.2.2
141
+ jupyterlab-widgets==1.1.1
142
+ MarkupSafe==2.1.1
143
+ matplotlib-inline==0.1.3
144
+ mistune==0.8.4
145
+ nbclient==0.6.6
146
+ nbconvert==6.5.0
147
+ nbformat==5.4.0
148
+ nest-asyncio==1.5.5
149
+ notebook==6.4.12
150
+ numpy==1.23.1
151
+ packaging==21.3
152
+ pandas==1.4.3
153
+ pandocfilters==1.5.0
154
+ parso==0.8.3
155
+ patsy==0.5.2
156
+ pickleshare==0.7.5
157
+ Pillow==9.2.0
158
+ plotly==5.9.0
159
+ prometheus-client==0.14.1
160
+ prompt-toolkit==3.0.30
161
+ protobuf==3.20.1
162
+ psutil==5.9.1
163
+ pure-eval==0.2.2
164
+ pyarrow==8.0.0
165
+ pycparser==2.21
166
+ pydeck==0.7.1
167
+ Pygments==2.12.0
168
+ Pympler==1.0.1
169
+ pyparsing==3.0.9
170
+ pyrsistent==0.18.1
171
+ python-dateutil==2.8.2
172
+ pytz==2022.1
173
+ pytz-deprecation-shim==0.1.0.post0
174
+ pyzmq==23.2.0
175
+ qtconsole==5.3.1
176
+ QtPy==2.1.0
177
+ requests==2.28.1
178
+ rich==12.5.1
179
+ scipy==1.8.1
180
+ semver==2.13.0
181
+ Send2Trash==1.8.0
182
+ six==1.16.0
183
+ smmap==5.0.0
184
+ soupsieve==2.3.2.post1
185
+ stack-data==0.3.0
186
+ statsmodels==0.13.2
187
+ streamlit==1.11.0
188
+ tenacity==8.0.1
189
+ terminado==0.15.0
190
+ tinycss2==1.1.1
191
+ toml==0.10.2
192
+ toolz==0.12.0
193
+ tornado==6.2
194
+ traitlets==5.3.0
195
+ typing_extensions==4.3.0
196
+ tzdata==2022.1
197
+ tzlocal==4.2
198
+ urllib3==1.26.11
199
+ validators==0.20.0
200
+ watchdog==2.1.9
201
+ wcwidth==0.2.5
202
+ webencodings==0.5.1
203
+ widgetsnbextension==3.6.1
204
+ zipp==3.8.1
src/app.py ADDED
@@ -0,0 +1,407 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+
4
+ import streamlit as st
5
+
6
+ import tools
7
+
8
+ STEP_2 = STEP_3 = STEP_4 = STEP_5 = STEP_6 = False
9
+
10
+
11
+ st.set_page_config(
12
+ page_title="A/B Tests", page_icon="📈", initial_sidebar_state="expanded"
13
+ )
14
+
15
+ st.title('A/B tests lab')
16
+
17
+ st.image('images/main.jpg')
18
+
19
+ st.write(
20
+ """
21
+ *Внедрять компании новый сервис или нет? Как принять правильное решение?*
22
+
23
+ *Поможет А/В-тестирование.*
24
+
25
+ A/B-тестирование, или сплит-тестирование (англ. A/B testing; Split testing, от англ. «разделять») —
26
+ техника проверки гипотез. Позволяет оценить, как изменение сервиса или продукта повлияет на пользователей.
27
+
28
+ Проводится так: аудиторию делят на две группы — контрольную (A) и тестовую (В). Группа A видит начальный сервис,
29
+ без изменений. Группа B получает новую версию, которую и нужно протестировать.
30
+ Эксперимент длится фиксированное время или по количеству пользователей.
31
+ В ходе тестирования собираются данные о поведении пользователей в разных группах.
32
+ Если ключевая метрика в тестовой группе выросла по сравнению с контрольной, новую функциональность внедряют.
33
+ """
34
+ )
35
+
36
+ st.image('images/ab-structure.png', width=700)
37
+
38
+ st.write(
39
+ """
40
+ Кому нужно A/B-тестирование
41
+
42
+ 1. _Продакт-менеджеры_ могут тестировать изменения ценовых моделей, направленные на повышение доходов, или оптимизацию части воронки продаж для увеличения конверсии.
43
+
44
+ 2. _Маркетологи_ могут тестировать изображения, призывы к действию (call-to-action) или практически любые другие элементы маркетинговой кампании или рекламы с точки зрения улучшения метрик.
45
+
46
+ 3. _Продуктовые дизайнеры_ могут тестировать дизайнерские решения (например, цвет кнопки оформления заказа) или использовать результаты тестирования для того, чтобы перед внедрением определить, будет ли удобно пользоваться новой функцией.
47
+ """
48
+ )
49
+
50
+ st.markdown(
51
+ """
52
+ Вот шесть шагов, которые нужно пройти, чтобы провести тестирование.
53
+ В некоторые из пунктов включены примеры тестирования страницы регистрации выдуманного стартапа.
54
+ """
55
+ )
56
+
57
+ with st.expander('Шаг 1. Определите цели', expanded=True):
58
+ st.write(
59
+ """
60
+ Определите основные бизнес-задачи вашей компании и убедитесь, что цели A/B-тестирования с ними совпадают.
61
+
62
+ Например, можем выпустить обновление приложения и проверить на маленькой группе,
63
+ что обновление не портит пользовательский опыт. Если метрики не падают, можем выкатывать обновление на всех.
64
+
65
+ """
66
+ )
67
+
68
+ purpose = st.radio(
69
+ 'Цели',
70
+ options=[
71
+ 'Занять делом скучающих сотрудников',
72
+ 'Решить проблему пользователей',
73
+ 'Снизить риски при значительных изменениях',
74
+ 'Обеспечить статистически значимые улучшения'
75
+ ]
76
+ )
77
+
78
+ match purpose:
79
+ case 'Занять делом скучающих сотрудников':
80
+ st.error(
81
+ """
82
+ Этой цели мы безусловно добьемся, но бизнесу от этого легче не станет.
83
+ """
84
+ )
85
+ case 'Решить проблему пользователей':
86
+ st.info(
87
+ """
88
+ Посетители приходят на сайт с конкретной целью: больше узнать о продукте или услуге, что-то купить,
89
+ изучить тему или просто поглазеть. При этом пользователи с разными целями сталкиваются с
90
+ общими проблемами. Наприме��, кнопка «Купить» расположена неудобно и её сложно найти.
91
+ Такие нюансы формируют негативный пользовательский опыт (пользоваться сайтом неудобно)
92
+ и влияют на конверсию.
93
+
94
+ Это актуально для всех сфер: будь то электронная коммерция, туризм, SaaS, образование,
95
+ СМИ или издательский бизнес.
96
+ """
97
+ )
98
+ st.error('Да, но сегодня мы будем добиваться другой цели. Выберите другую.')
99
+
100
+ case 'Снизить риски при значительных изменениях':
101
+ st.info(
102
+ """
103
+ Рекомендуем вносить небольшие и последовательные изменения вместо того, чтобы одновременно делать
104
+ редизайн всей страницы. Так снизится вероятность ухудшения коэффициента конверсии.
105
+
106
+ A/B-тесты позволяют получать хороший результат и при этом вносить лишь небольшие изменения,
107
+ что приводит к увеличению ROI.
108
+
109
+ В качестве примера приведём изменения в описании продукта. Вы можете сделать A/B-тест,
110
+ когда нужно удалить или обновить описание продукта, но при этом не знаете, как посетители будут
111
+ реагировать на это.
112
+
113
+ Другой пример модификации с низким риском — добавление новой функции. A/B-тест поможет
114
+ сделать результат внедрения более предсказуемым.
115
+ """
116
+ )
117
+ st.error('Да, но сегодня мы будем добиваться другой цели. Выберите другую.')
118
+
119
+ case 'Обеспечить статистически значимые улучшения':
120
+ st.info(
121
+ """
122
+ A/B-тестирование полностью основано на данных и не оставляет места для догадок.
123
+ Поэтому можно легко определить «победителя» и «проигравшего» на основе статистически значимых
124
+ улучшений: показателей времени на странице, число запросов пробников, количество
125
+ брошенных корзин, CTR.
126
+ """
127
+ )
128
+ st.success('Да, попробуем добиться статистически значимого улучшения метрики.')
129
+ STEP_2 = True
130
+
131
+
132
+ if STEP_2:
133
+ with st.expander('Шаг 2. Определите метрику', expanded=True):
134
+ st.write(
135
+ """
136
+ На данном этапе необходимо определить метрику, на которую вы будете смотреть, чтобы понять, является ли
137
+ новая версия сайта более успешной, чем изначальная. Обычно в качестве такой метрики берут
138
+ коэффициент конверсии, но можно выбрать и промежуточную метрику вроде показателя кликабельности (CTR).
139
+ """
140
+ )
141
+
142
+ metrick = st.radio(
143
+ 'Цели',
144
+ options=[
145
+ 'Обеспечить лучшую окупаемость инвестиций (ROI)',
146
+ 'Уменьшить показатель отказов',
147
+ 'Повысить конверсию',
148
+ ]
149
+ )
150
+
151
+ match metrick:
152
+ case 'Обеспечить лучшую окупаемость инвестиций (ROI)':
153
+ st.info(
154
+ """
155
+ Маркетологи знают, каким дорогим бывает качественный трафик. A/B-тестирование позволяет эффективно
156
+ использовать существующий трафик и помогает повысить конверсию без затрат на привлечение нового.
157
+ Иногда даже незначительные изменения влияют на конверсию.
158
+ """
159
+ )
160
+ st.error('Сегодня мы будем тестировать не эту метрику. Выберите другую.')
161
+
162
+ case 'Уменьшить показатель отказов':
163
+ st.info(
164
+ """
165
+ Для оценки эффективности сайта важно отслеживать показатель отказов.
166
+ Люди покидают сайт по разным причинам: слишком много вариантов товара, несоответствие ожиданиям
167
+ и другие. Поскольку сайты различаются по аудиториям и целям, нет универсального надёжного способа
168
+ определения показателя отказов.
169
+
170
+ Но решение есть: в каждом случае поможет A/B-тестирование. Можно протестировать несколько вариантов расположения
171
+ элементов на сайте и найти оптимальное решение.
172
+ """
173
+ )
174
+ st.error('Сегодня мы будем тестировать не эту метрику. Выберите другую.')
175
+
176
+ case 'Повысить конверсию':
177
+ st.info(
178
+ """
179
+ Конверсия — один из главных терминов в маркетинге. Не считая конверсию, сложно
180
+ оценить эффективность маркетинга и работать с воронкой продаж.
181
+
182
+ Конверсия показывает, какой процент пользователей или потенциальных клиентов совершили
183
+ целевое действие: оставили заявку, купили товар, подписались на рассылку и так далее.
184
+ """
185
+ )
186
+ st.success('Правильно! Именно эту метрику мы и будем оптимизировать')
187
+ STEP_3 = True
188
+
189
+ if STEP_3:
190
+ with st.expander('Шаг 3. Разработайте гипотезу', expanded=True):
191
+ st.write(
192
+ """
193
+ Затем нужно разработать гипотезу о том, что именно поменяется, и, соответственно, что вы хотите проверить.
194
+ Нужно понять, каких результатов вы ожидаете и какие у них могут быть обоснования.
195
+
196
+ Нужно определить две гипотезы, которые помогут понять, является ли наблюдаемая разница между версией
197
+ A (изначальной) и версией B (новой, которую вы хотите проверить) случайностью или результатом изменений,
198
+ которые вы произвели.
199
+
200
+ * _Нулевая гипотеза_ предполагает, что результаты, А и В на самом деле не отличаются и что наблюдаемые различия случайны. Мы надеемся опровергнуть эту гипотезу.
201
+
202
+ * _Альтернативная гипотеза_ — это гипотеза о том, что B отличается от A, и вы хотите сделать вывод об её истинности.
203
+
204
+ Решите, будет ли это односторонний или двусторонний тест.
205
+ Односторонний тест позволяет обнаружить изменение в одном направлении,
206
+ в то время как двусторонний тест позволяет обнаружить изменение по двум направлениям
207
+ (как положительное, так и отрицательное).
208
+ """
209
+ )
210
+
211
+ st.radio(
212
+ "Тип теста",
213
+ options=["Односторонний", "Двусторонний"],
214
+ index=0,
215
+ key="hypothesis",
216
+ help="Односторонний тест позволяет обнаружить изменение в одном направлении, в то время как двусторонний тест позволяет обнаружить изменение по двум направлениям (как положительное, так и отрицательное). ",
217
+ )
218
+
219
+ STEP_4 = True
220
+
221
+ if STEP_4:
222
+ with st.expander('Шаг 4. Подготовьте эксперимент', expanded=True):
223
+ st.write(
224
+ """
225
+ 1. _Создайте новую версию (B)_, отражающую изменения, которые вы хотите протестировать.
226
+
227
+ 2. _Определите контрольную и экспериментальную группы_.
228
+
229
+ Каких пользователей вы хотите протестировать:
230
+ всех пользователей на всех платформах или только пользователей из одной страны? Определите группу испытуемых,
231
+ отобрав их по типам пользователей, платформе, географическим показателям и т.п.
232
+ Затем определите, какой процент исследуемой группы составляет контрольная группа (групп��, видящая версию A),
233
+ а какой процент — экспериментальная группа (группа, видящая версию B). Обычно эти группы одинакового размера.
234
+
235
+ 3. _Убедитесь, что пользователи будут видеть версии A и B в случайном порядке_.
236
+
237
+ Это значит, у каждого пользователя будет равный шанс получить ту или иную версию.
238
+
239
+ 4. _Определите уровень статистической значимости (α)_.
240
+
241
+ Это уровень риска, который вы принимаете при ошибках первого рода (отклонение нулевой гипотезы, если она верна), обычно α = 0.05.
242
+ Это означает, что в 5% случаев вы будете обнаруживать разницу между A и B,
243
+ которая на самом деле обусловлена случайностью. Чем ниже выбранный вами уровень значимости,
244
+ тем ниже риск того, что вы обнаружите разницу, вызванную случайностью.
245
+
246
+ 5. _Определите минимальный размер выборки_. Калькулятор есть [здесь](https://vwo.com/tools/ab-test-sample-size-calculator/).
247
+
248
+ Он рассчитывают размер выборки, необходимый для каждой версии. На размер выборки влияют разные параметры и ваши предпочтения.
249
+ Наличие достаточно большого размера выборки важно для обеспечения статистически значимых результатов.
250
+
251
+ 6. _Определите временные рамки_. Калькулятор есть [здесь](https://vwo.com/tools/ab-test-duration-calculator/).
252
+
253
+ Возьмите общий размер выборки, необходимый вам для тестирования каждой версии,
254
+ и разделите его на ваш ежедневный трафик. Так вы получите количество дней,
255
+ необходимое для проведения теста. Как правило, это одна или две недели.
256
+
257
+ У A/B-теста есть проблема подглядывания (англ. peeking problem): общий результат искажается, если новые данные
258
+ поступают в начале эксперимента. Каждый, даже небольшой фрагмент новых данных, велик относительно уже
259
+ накопленных — статистическая значимость достигается за короткий срок.
260
+ """
261
+ )
262
+
263
+ st.image('images/peeking_problem.png', width=670)
264
+
265
+ st.write(
266
+ """
267
+ На графике разница конверсии между сегментами, полученная в результате смоделированного A/B-теста.
268
+ Данные собирали из одной генеральной совокупности, и различий в выборочных средних быть не должно.
269
+ Но из-за флуктуаций (от лат. fluctuatio, колебание) в первые дни тестирования была достигнута
270
+ статистическая значимость. Если бы это был реальный, а не смоделированный тест, принятое по достижении
271
+ статистической значимости решение было бы неверным.
272
+
273
+ Чтобы избежать проблемы подглядывания, размер выборки определяют ещё до начала теста.
274
+ """
275
+ )
276
+
277
+ st.slider(
278
+ "Уровень значимости (α)",
279
+ min_value=0.01,
280
+ max_value=0.10,
281
+ value=0.05,
282
+ step=0.01,
283
+ key="alpha",
284
+ help="Это уровень риска, который вы принимаете при ошибках первого рода (отклонение нулевой гипотезы, если она верна), обычно α = 0.05.",
285
+ )
286
+
287
+ ab_test_duration = st.select_slider(label='Выберите длительность A/B теста в днях', options=range(3, 31))
288
+ mean_traff = st.number_input(label='Укажите, среднюю посещаемость сайта в сутки', min_value=150)
289
+ ab_test_sample_size = st.select_slider(label=f'Укажите размер выборки для группы B (при 20% от средней посещаемости в день, максимальный размер выборки для группы B - {int(mean_traff * 0.2 * ab_test_duration)})', options=range(60, int(mean_traff * 0.2 * ab_test_duration) + 1))
290
+ st.write(f'Выбрано ~{int((ab_test_sample_size / ab_test_duration) / mean_traff * 100)}% от средней посещаемости в сутки.')
291
+
292
+ STEP_5 = True
293
+
294
+ if STEP_5:
295
+ with st.expander('Шаг 5. Проведите эксперимент', expanded=True):
296
+ st.write(
297
+ """
298
+ Помните о важных шагах, которые необходимо выполнить:
299
+
300
+ 1. Обсудите параметры эксперимента с исполнителями.
301
+ 2. Выполните запрос на тестовой закрытой площадке, если она у вас есть. Это поможет проверить данные. Если ее нет, проверьте данные, полученные в первый день эксперимента.
302
+ 3. В самом начале проведения тестирования проверьте, действительно ли оно работает.
303
+ 4. И наконец, не смотрите на результаты!
304
+
305
+ Преждевременный просмотр результатов может испортить статистическую значимость.
306
+ """
307
+ )
308
+
309
+ with st.form(key='start_ab'):
310
+ start_test = st.form_submit_button('Провести тест')
311
+ if start_test:
312
+
313
+ st.write("Посмотрим на проведенный тест")
314
+
315
+ df = tools.get_dataset(ab_test_sample_size, ab_test_duration)
316
+
317
+ visitors_a = df[df['group'] == 'old_version'].shape[0]
318
+ visitors_b = df[df['group'] == 'new_version'].shape[0]
319
+
320
+ conversions_a = df.groupby(['group', 'converted']).agg('count')['user_id'][3]
321
+ conversions_b = df.groupby(['group', 'converted']).agg('count')['user_id'][1]
322
+
323
+ st.write(df.sample(7))
324
+
325
+ st.plotly_chart(tools.get_plotly_converted_hist(df), use_container_width=True)
326
+
327
+ STEP_6 = True
328
+
329
+ if STEP_6:
330
+ with st.expander('Шаг 6. Проанализируйте результаты', expanded=True):
331
+ st.write(
332
+ """
333
+ Вам нужно получить данные и рассчитать значения выбранной ранее метрики успеха для обеих версий
334
+ (A и B) и разницу между этими значениями.
335
+ Если не было никакой разницы в целом, вы также можете сегментировать выборку по платформам, типам источников,
336
+ географическим параметрам и т.п., если это применимо. Вы можете обнаружить,
337
+ что версия B работает лучше или хуже для определенных сегментов.
338
+
339
+ Проверьте статистическую значимость. Статистическая теория, лежащая в основе этого подхода, объясняется здесь,
340
+ но основная идея в том, чтобы выяснить, была ли разница в результатах между A и B связана с изменениями
341
+ или это результат случайности либо естественных изменений. Это определяется путем сравнения
342
+ тестовых статистических данных (и полученного p-значения) с вашим уровнем значимости.
343
+
344
+ Если p-значение меньше уровня значимости, то можно отвергнуть нулевую гипотезу, если имеются
345
+ доказательства для альтернативы.
346
+
347
+ Если p-значение больше или равно уровню значимости, мы не можем отвергнуть нулевую гипотезу о том,
348
+ что A и B не отличаются друг от друга.
349
+ """
350
+ )
351
+
352
+ tools.calculate_significance(
353
+ conversions_a,
354
+ conversions_b,
355
+ visitors_a,
356
+ visitors_b
357
+ )
358
+
359
+ mcol1, mcol2 = st.columns(2)
360
+
361
+ with mcol1:
362
+ st.metric(
363
+ "Разница",
364
+ value=f"{(st.session_state.crb - st.session_state.cra):.3g}%",
365
+ delta=f"{(st.session_state.crb - st.session_state.cra):.3g}%",
366
+ )
367
+
368
+ with mcol2:
369
+ st.metric("Различие статзначимо?", value=st.session_state.significant)
370
+
371
+ results_df = pd.DataFrame(
372
+ {
373
+ "Group": ["A", "B"],
374
+ "Conversion": [st.session_state.cra, st.session_state.crb],
375
+ }
376
+ )
377
+ tools.plot_chart(results_df)
378
+
379
+ table = pd.DataFrame(
380
+ {
381
+ "Converted": [conversions_a, conversions_b],
382
+ "Total": [visitors_a, visitors_b],
383
+ "% Converted": [st.session_state.cra, st.session_state.crb],
384
+ },
385
+ index=pd.Index(["A", "B"]),
386
+ )
387
+
388
+ st.write(table.style.format(formatter={("% Converted"): "{:.3g}%"}))
389
+
390
+ metrics = pd.DataFrame(
391
+ {
392
+ "p-value": [st.session_state.p],
393
+ "z-score": [st.session_state.z],
394
+ "uplift": [st.session_state.uplift],
395
+ },
396
+ index=pd.Index(["Metrics"]),
397
+ )
398
+
399
+ st.write(
400
+ metrics.style.format(
401
+ formatter={("p-value", "z-score"): "{:.3g}", ("uplift"): "{:.3g}%"}
402
+ )
403
+ .applymap(tools.style_negative, props="color:red;")
404
+ .apply(tools.style_p_value, props="color:red;", axis=1, subset=["p-value"])
405
+ )
406
+ st.plotly_chart(tools.get_fig(df), use_container_width=True)
407
+
src/tools.py ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from uuid import uuid4
2
+ from datetime import datetime, timedelta
3
+
4
+ import plotly.express as px
5
+ import plotly.graph_objects as go
6
+ import numpy as np
7
+ import pandas as pd
8
+ from scipy.stats import t
9
+ from scipy.stats import norm
10
+ import altair as alt
11
+ import plotly.express as px
12
+ import streamlit as st
13
+
14
+
15
+ def conversion_rate(conversions, visitors):
16
+ return (conversions / visitors) * 100
17
+
18
+
19
+ def lift(cra, crb):
20
+ return ((crb - cra) / cra) * 100
21
+
22
+
23
+ def std_err(cr, visitors):
24
+ return np.sqrt((cr / 100 * (1 - cr / 100)) / visitors)
25
+
26
+
27
+ def std_err_diff(sea, seb):
28
+ return np.sqrt(sea ** 2 + seb ** 2)
29
+
30
+
31
+ def z_score(cra, crb, error):
32
+ return ((crb - cra) / error) / 100
33
+
34
+
35
+ def p_value(z, hypothesis):
36
+ if hypothesis == "One-sided" and z < 0:
37
+ return 1 - norm().sf(z)
38
+ elif hypothesis == "One-sided" and z >= 0:
39
+ return norm().sf(z) / 2
40
+ else:
41
+ return norm().sf(z)
42
+
43
+
44
+ def significance(alpha, p):
45
+ return "YES" if p < alpha else "NO"
46
+
47
+
48
+ def plot_chart(df):
49
+ chart = (
50
+ alt.Chart(df)
51
+ .mark_bar(color="#61b33b")
52
+ .encode(
53
+ x=alt.X("Group:O", axis=alt.Axis(labelAngle=0)),
54
+ y=alt.Y("Conversion:Q", title="Conversion rate (%)"),
55
+ opacity="Group:O",
56
+ )
57
+ .properties(width=500, height=500)
58
+ )
59
+
60
+ chart_text = chart.mark_text(
61
+ align="center", baseline="middle", dy=-10, color="black"
62
+ ).encode(text=alt.Text("Conversion:Q", format=",.3g"))
63
+
64
+ return st.altair_chart((chart + chart_text).interactive())
65
+
66
+
67
+ def style_negative(v, props=""):
68
+ return props if v < 0 else None
69
+
70
+
71
+ def style_p_value(v, props=""):
72
+ return np.where(v < st.session_state.alpha, "color:green;", props)
73
+
74
+
75
+ def calculate_significance(
76
+ conversions_a, conversions_b, visitors_a, visitors_b
77
+ ):
78
+ st.session_state.cra = conversion_rate(int(conversions_a), int(visitors_a))
79
+ st.session_state.crb = conversion_rate(int(conversions_b), int(visitors_b))
80
+ st.session_state.uplift = lift(st.session_state.cra, st.session_state.crb)
81
+ st.session_state.sea = std_err(st.session_state.cra, float(visitors_a))
82
+ st.session_state.seb = std_err(st.session_state.crb, float(visitors_b))
83
+ st.session_state.sed = std_err_diff(st.session_state.sea, st.session_state.seb)
84
+ st.session_state.z = z_score(
85
+ st.session_state.cra, st.session_state.crb, st.session_state.sed
86
+ )
87
+ st.session_state.p = p_value(st.session_state.z, st.session_state.hypothesis)
88
+ st.session_state.significant = significance(
89
+ st.session_state.alpha, st.session_state.p
90
+ )
91
+
92
+
93
+ def get_dataset(size, days) -> pd.DataFrame:
94
+
95
+ end = datetime.today()
96
+ start = end - timedelta(days=days)
97
+
98
+ data = pd.DataFrame(data={
99
+ 'user_id': [str(uuid4()) for _ in range(size)],
100
+ 'group': np.random.choice(['old_version', 'new_version'], size=size),
101
+ 'timestamp': pd.date_range(start=start, end=end, periods=size)
102
+ })
103
+
104
+ old_version_index = data[data['group'] == 'old_version'].index
105
+ new_version_index = data[data['group'] == 'new_version'].index
106
+
107
+ data.loc[old_version_index, 'converted'] = np.random.choice(
108
+ [0, 1],
109
+ size=(len(old_version_index), 1),
110
+ p=[0.8, 0.2]
111
+ )
112
+
113
+ data.loc[new_version_index, 'converted'] = np.random.choice(
114
+ [0, 1],
115
+ size=(len(new_version_index), 1),
116
+ p=[0.75, 0.25]
117
+ )
118
+
119
+ data['converted'] = data['converted'].astype('int')
120
+
121
+ data.loc[old_version_index, 'avg_check'] = np.random.normal(
122
+ size=len(old_version_index),
123
+ loc=15,
124
+ scale=7
125
+ )
126
+
127
+ data.loc[new_version_index, 'avg_check'] = np.random.normal(
128
+ size=len(new_version_index),
129
+ loc=17,
130
+ scale=6.4
131
+ )
132
+
133
+ return data
134
+
135
+
136
+ def get_plotly_converted_hist(data: pd.DataFrame):
137
+
138
+ fig = go.Figure()
139
+
140
+ fig.add_trace(
141
+ go.Histogram(
142
+ dict(
143
+ x=data[data['group'] == 'old_version']['converted'].map({1: 'Да', 0: 'Нет'}),
144
+ name='old_version'
145
+ )
146
+ )
147
+ )
148
+
149
+ fig.add_trace(
150
+ go.Histogram(
151
+ dict(
152
+ x=data[data['group'] == 'new_version']['converted'].map({1: 'Да', 0: 'Нет'}),
153
+ name='new_version'
154
+ )
155
+ )
156
+ )
157
+
158
+ fig.update_traces(hovertemplate="Сконвертирован: %{x}<br>"
159
+ "Количество: %{y}")
160
+
161
+ fig.update_layout(
162
+ title='Распределение конверсий в новой и старой версии сайта'
163
+ )
164
+
165
+ fig.update_xaxes(
166
+ title='Сконвертирован'
167
+ )
168
+
169
+ fig.update_yaxes(
170
+ title='Количество'
171
+ )
172
+
173
+ return fig
174
+
175
+
176
+ def get_fig(df: pd.DataFrame):
177
+
178
+ p = []
179
+ x = []
180
+ with st.spinner('Строю график статзначимости...'):
181
+ for i in range(50, df.shape[0]):
182
+ visitors_a = df.loc[:i][df['group'] == 'old_version'].shape[0]
183
+ visitors_b = df.loc[:i][df['group'] == 'new_version'].shape[0]
184
+
185
+ conversions_a = df.loc[:i].groupby(['group', 'converted']).agg('count')['user_id'][3]
186
+ conversions_b = df.loc[:i].groupby(['group', 'converted']).agg('count')['user_id'][1]
187
+
188
+ calculate_significance(
189
+ conversions_a,
190
+ conversions_b,
191
+ visitors_a,
192
+ visitors_b
193
+ )
194
+ p.append(np.round(p_value(st.session_state.z, st.session_state.hypothesis) * 100, 2))
195
+ x.append(df['timestamp'].iloc[i])
196
+
197
+ fig = px.line(
198
+ x=x,
199
+ y=p,
200
+ title='Зависимость статзначимости от времени проведения эксперимента')
201
+
202
+ fig.update_xaxes(
203
+ title='Количество пользователей'
204
+ )
205
+
206
+ fig.update_yaxes(
207
+ title='p-value'
208
+ )
209
+
210
+ fig.update_layout(
211
+ showlegend=False
212
+ )
213
+
214
+ fig.add_hline(
215
+ y=st.session_state.alpha * 100,
216
+ line_color='green',
217
+ line_dash='dash'
218
+ )
219
+
220
+ fig.update_traces(hovertemplate="Время А/B теста: %{x}<br>"
221
+ "Достигнутая статзначимость: %{y}%")
222
+
223
+ return fig
224
+
225
+
226
+ def get_interval(data):
227
+ return t.interval(
228
+ alpha=st.session_state.alpha,
229
+ df=2,
230
+ loc=data['avg_check'].mean(),
231
+ scale=data['avg_check'].sem()
232
+ )