Spaces:
Runtime error
Runtime error
HardWorkingStation
commited on
Commit
·
853a5d2
0
Parent(s):
Initial commit
Browse files- .gitattributes +1 -0
- .github/workflow/main.yaml +30 -0
- .gitignore +1 -0
- README.md +10 -0
- data/ab_test.csv +3 -0
- images/ab-duration.png +0 -0
- images/ab-structure.png +0 -0
- images/hypotesis.png +0 -0
- images/main.jpg +0 -0
- images/peeking_problem.png +0 -0
- requirements.txt +204 -0
- src/app.py +407 -0
- src/tools.py +232 -0
.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 |
+
)
|