{"id":473243,"date":"2025-09-02T15:55:25","date_gmt":"2025-09-02T15:55:25","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=473243"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=473243","title":{"rendered":"<span>\u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0410\/\u0411-\u0442\u0435\u0441\u0442\u043e\u0432<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p><em>\u0414\u043b\u044f \u0410\/\u0411-\u0442\u0435\u0441\u0442\u043e\u0432 \u0432 \u0432\u0435\u0431\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u044b \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u0439 \u0432\u044b\u0431\u043e\u0440 \u0433\u0440\u0443\u043f\u043f, \u0445\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435, \u043b\u043e\u0433\u0438\u043a\u0430 \u043d\u0430 \u0431\u044d\u043a\u044d\u043d\u0434\u0435 \u0438 \u0444\u0440\u043e\u043d\u0442\u044d\u043d\u0434\u0435, \u043b\u043e\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u0439, \u043e\u0434\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0435 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u044b \u0438 \u0430\u0434\u043c\u0438\u043d\u043a\u0430.<\/em><\/p>\n<p><a href=\"#%D0%A1%D0%BB%D1%83%D1%87%D0%B0%D0%B9%D0%BD%D1%8B%D0%B5-%D0%B3%D1%80%D1%83%D0%BF%D0%BF%D1%8B\" rel=\"noopener noreferrer nofollow\"><em>1. \u0421\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u0435 \u0433\u0440\u0443\u043f\u043f\u044b<\/em><\/a><br \/><a href=\"#%D0%A5%D1%8D%D1%88%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5\" rel=\"noopener noreferrer nofollow\"><em>2. \u0425\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435<\/em><\/a><br \/><a href=\"#%D0%A4%D1%80%D0%BE%D0%BD%D1%82%D1%8D%D0%BD%D0%B4\" rel=\"noopener noreferrer nofollow\"><em>3. \u0424\u0440\u043e\u043d\u0442\u044d\u043d\u0434<\/em><\/a><br \/><a href=\"#%D0%A1%D0%BE%D0%B1%D1%8B%D1%82%D0%B8%D1%8F\" rel=\"noopener noreferrer nofollow\"><em>4. \u0421\u043e\u0431\u044b\u0442\u0438\u044f<\/em><\/a><br \/><a href=\"#%D0%9A%D0%BE%D0%BD%D1%84%D0%B8%D0%B3\" rel=\"noopener noreferrer nofollow\"><em>5. \u041a\u043e\u043d\u0444\u0438\u0433<\/em><\/a><br \/><a href=\"#%D0%94%D0%B2%D0%B0-%D1%8D%D0%BA%D1%81%D0%BF%D0%B5%D1%80%D0%B8%D0%BC%D0%B5%D0%BD%D1%82%D0%B0\" rel=\"noopener noreferrer nofollow\"><em>6. \u0414\u0432\u0430 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430<\/em><\/a><br \/><a href=\"#%D0%90%D0%B4%D0%BC%D0%B8%D0%BD%D0%BA%D0%B0\" rel=\"noopener noreferrer nofollow\"><em>7. \u0410\u0434\u043c\u0438\u043d\u043a\u0430<\/em><\/a><br \/><a href=\"#%D0%92%D0%B5%D1%81%D0%B0\" rel=\"noopener noreferrer nofollow\"><em>8. \u0412\u0435\u0441\u0430<\/em><\/a><br \/><a href=\"#%D0%A0%D0%B0%D1%81%D0%BA%D0%B0%D1%82%D0%BA%D0%B0\" rel=\"noopener noreferrer nofollow\"><em>9. \u0420\u0430\u0441\u043a\u0430\u0442\u043a\u0430<\/em><\/a><br \/><a href=\"#%D0%97%D0%B0%D0%BA%D0%BB%D1%8E%D1%87%D0%B5%D0%BD%D0%B8%D0%B5\" rel=\"noopener noreferrer nofollow\"><em>\u0417\u0430\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435<\/em><\/a><\/p>\n<p>\u0420\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0439: <a href=\"https:\/\/github.com\/andrewbrdk\/AB-Testing-from-Scratch\" rel=\"noopener noreferrer nofollow\">https:\/\/github.com\/andrewbrdk\/AB-Testing-from-Scratch<\/a> .<\/p>\n<p>\u0412\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0435 \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u0435 \u043f\u0440\u0438\u043c\u0435\u0440\u043e\u0432:<\/p>\n<pre><code class=\"bash\">git clone https:\/\/github.com\/andrewbrdk\/Web-AB-Testing-Demo cd Web-AB-Testing-Demo python -m venv pyvenv source .\/pyvenv\/bin\/activate pip install flask aiohttp playwright playwright install chromium<\/code><\/pre>\n<p>A\/\u0411-\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0432\u0435\u0431-\u0441\u0435\u0440\u0432\u0438\u0441\u043e\u0432 \u043e\u0446\u0435\u043d\u0438\u0432\u0430\u0435\u0442 \u0432\u043b\u0438\u044f\u043d\u0438\u0435 \u043d\u043e\u0432\u044b\u0445 \u0444\u0443\u043d\u043a\u0446\u0438\u0439 \u043d\u0430 \u043a\u043b\u044e\u0447\u0435\u0432\u044b\u0435 \u043c\u0435\u0442\u0440\u0438\u043a\u0438. \u041e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u044c\u043d\u0430\u044f \u0438 \u0438\u0437\u043c\u0435\u043d\u0451\u043d\u043d\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u044e\u0442\u0441\u044f \u043f\u0430\u0440\u0430\u043b\u043b\u0435\u043b\u044c\u043d\u043e, \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438 \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u043e \u0434\u0435\u043b\u044f\u0442\u0441\u044f \u043c\u0435\u0436\u0434\u0443 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0430\u043c\u0438. \u041e\u0434\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0439 \u0437\u0430\u043f\u0443\u0441\u043a \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432\u0430\u0435\u0442 \u0440\u0430\u0432\u043d\u043e\u0435 \u0432\u043b\u0438\u044f\u043d\u0438\u0435 \u0432\u043d\u0435\u0448\u043d\u0438\u0445 \u0444\u0430\u043a\u0442\u043e\u0440\u043e\u0432, \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u043e\u0435 \u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u0441\u043d\u0438\u0436\u0430\u0435\u0442 \u0434\u0438\u0437\u0431\u0430\u043b\u0430\u043d\u0441 \u0433\u0440\u0443\u043f\u043f. \u0412 \u0438\u0442\u043e\u0433\u0435 \u0440\u0430\u0437\u043b\u0438\u0447\u0438\u044f \u043c\u0435\u0442\u0440\u0438\u043a \u043c\u0435\u0436\u0434\u0443 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0430\u043c\u0438 \u043e\u0431\u044a\u044f\u0441\u043d\u044f\u044e\u0442 \u043d\u043e\u0432\u043e\u0439 \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c\u044e.<\/p>\n<p>\u0414\u043b\u044f \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e\u0439 \u043e\u0446\u0435\u043d\u043a\u0438 \u044d\u0444\u0444\u0435\u043a\u0442\u0430 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438 \u0434\u043e\u043b\u0436\u043d\u044b \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u043e\u0432\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u0441 \u043e\u0434\u043d\u043e\u0439 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0435\u0439, \u043f\u043e\u043f\u0430\u0432\u0448\u0438\u0445 \u0432 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0433\u0440\u0443\u043f\u043f \u043d\u0443\u0436\u043d\u043e \u0438\u0441\u043a\u043b\u044e\u0447\u0430\u0442\u044c \u043f\u0440\u0438 \u043f\u043e\u0434\u0432\u0435\u0434\u0435\u043d\u0438\u0438 \u0438\u0442\u043e\u0433\u043e\u0432. \u041d\u0430 \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0435 \u0433\u0440\u0443\u043f\u043f\u0443 \u043f\u0440\u0438\u0432\u044f\u0437\u044b\u0432\u0430\u044e\u0442 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 \u0438\u043b\u0438 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0443, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043f\u0440\u0438 \u0441\u043c\u0435\u043d\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043c\u043e\u0436\u0435\u0442 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c\u0441\u044f \u0434\u0440\u0443\u0433\u043e\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442.<\/p>\n<p><a class=\"anchor\" name=\"%D0%A1%D0%BB%D1%83%D1%87%D0%B0%D0%B9%D0%BD%D1%8B%D0%B5-%D0%B3%D1%80%D1%83%D0%BF%D0%BF%D1%8B\" id=\"\u0421\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u0435-\u0433\u0440\u0443\u043f\u043f\u044b\"><\/a><\/p>\n<h2>1. \u0421\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u0435 \u0433\u0440\u0443\u043f\u043f\u044b<\/h2>\n<p>\u042d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442\u0441\u044f \u043d\u0430 \u0431\u044d\u043a\u044d\u043d\u0434\u0435 \u0432\u044b\u0437\u043e\u0432\u043e\u043c <code>random.choice<\/code>. \u0413\u0440\u0443\u043f\u043f\u0430 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442\u0441\u044f \u0432 \u043a\u0443\u043a\u0430\u0445 \u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442\u0441\u044f \u043f\u0440\u0438 \u043a\u0430\u0436\u0434\u043e\u043c \u0437\u0430\u0445\u043e\u0434\u0435 \u043d\u0430 \u0441\u0430\u0439\u0442 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e\u0441\u0442\u0438 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0430.<\/p>\n<pre><code class=\"bash\">python 1_rnd.py<\/code><\/pre>\n<p>\u042d\u043a\u0441\u043f: <a href=\"http:\/\/127.0.0.1:5000\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000<\/a><\/p>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/fd4\/899\/c46\/fd4899c460be2f32f0f0fe61f877278f.png\" alt=\"Moon, Mars\" title=\"\u0412\u0430\u0440\u0438\u0430\u043d\u0442\u044b \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430.\" width=\"1920\" height=\"1080\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/fd4\/899\/c46\/fd4899c460be2f32f0f0fe61f877278f.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/fd4\/899\/c46\/fd4899c460be2f32f0f0fe61f877278f.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>\u0412\u0430\u0440\u0438\u0430\u043d\u0442\u044b \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430.<\/figcaption><\/div>\n<\/figure>\n<pre><code class=\"python\">from flask import Flask, render_template_string, request, make_response import random  app = Flask(__name__)  TEMPLATE = ''' &lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt;     &lt;title&gt;A\/B Test&lt;\/title&gt;     &lt;link rel=\"stylesheet\" href=\"{{ url_for('static', filename='banners.css') }}\"&gt; &lt;\/head&gt; &lt;body&gt;     {% if variant == 'Moon' %}         &lt;div class=\"banner\" style=\"background-image: url('{{ url_for('static', filename='.\/moon.jpg') }}');\"&gt;             &lt;h1&gt;Walk on the Moon&lt;\/h1&gt;             &lt;div class=\"vspacer\"&gt;&lt;\/div&gt;             &lt;p&gt;Be one of the first tourists to set foot on the lunar surface. Your journey to another world starts here.&lt;\/p&gt;             &lt;button onclick=\"console.log('Click Moon')\"&gt;Reserve Your Spot&lt;\/button&gt;         &lt;\/div&gt;     {% else %}         &lt;div class=\"banner\" style=\"background-image: url('{{ url_for('static', filename='.\/mars.jpg') }}');\"&gt;             &lt;h1&gt;Journey to Mars&lt;\/h1&gt;             &lt;div class=\"vspacer\"&gt;&lt;\/div&gt;             &lt;p&gt;Be among the first humans to set foot on the Red Planet. Experience the adventure of a lifetime.&lt;\/p&gt;             &lt;button onclick=\"console.log('Click Mars')\"&gt;Reserve Your Spot&lt;\/button&gt;         &lt;\/div&gt;     {% endif %} &lt;\/body&gt; &lt;\/html&gt; '''  @app.route('\/') def index():     variant = request.cookies.get('variant')     if variant not in ['Moon', 'Mars']:         variant = random.choice(['Moon', 'Mars'])     response = make_response(render_template_string(TEMPLATE, variant=variant))     response.set_cookie('variant', variant, max_age=60*60*24*30)     return response  if __name__ == '__main__':     app.run(debug=True)<\/code><\/pre>\n<ul>\n<li>\n<p><code>{% if variant == 'Moon' %} ... {% endif %}<\/code> &#8212; \u0431\u044d\u043a\u0435\u043d\u0434 \u043e\u0442\u0434\u0430\u0451\u0442 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u0433\u0440\u0443\u043f\u043f\u0435 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b.<\/p>\n<\/li>\n<li>\n<p><code>variant = request.cookies.get('variant')<\/code> &#8212; \u0441\u0447\u0438\u0442\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0442\u0435\u043a\u0443\u0449\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430 \u0432 \u043a\u0443\u043a\u0430\u0445.<\/p>\n<\/li>\n<li>\n<p><code>variant = random.choice(['Moon', 'Mars'])<\/code> &#8212; \u0435\u0441\u043b\u0438 \u0433\u0440\u0443\u043f\u043f\u044b \u043d\u0435\u0442, \u0433\u0440\u0443\u043f\u043f\u0430 \u0432\u044b\u0431\u0438\u0440\u0430\u0435\u0442\u0441\u044f \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u043e.<\/p>\n<\/li>\n<li>\n<p><code>response.set_cookie('variant', variant, max_age=60*60*24*30)<\/code> &#8212; \u0433\u0440\u0443\u043f\u043f\u0430 \u0437\u0430\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0432 \u043a\u0443\u043a\u0438.<\/p>\n<\/li>\n<\/ul>\n<p>\u0414\u043b\u044f \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0430 \u0434\u0440\u0443\u0433\u043e\u0433\u043e \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0430 \u043d\u0443\u0436\u043d\u043e \u043b\u0438\u0431\u043e \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0432 \u043d\u043e\u0432\u043e\u043c \u043e\u043a\u043d\u0435 \u0438\u043d\u043a\u043e\u0433\u043d\u0438\u0442\u043e, \u043b\u0438\u0431\u043e \u043e\u0447\u0438\u0441\u0442\u0438\u0442\u044c \u043a\u0443\u043a\u0438 \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443.<\/p>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/141\/b77\/47c\/141b7747cfdbaf8dbdcd5b06289f6cb4.png\" alt=\"Clear Cookies\" title=\"\u042d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442\u0441\u044f \u0432 \u043a\u0443\u043a\u0430\u0445 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e\u0441\u0442\u0438 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0430 \u043f\u0440\u0438 \u0437\u0430\u0445\u043e\u0434\u0435 \u043d\u0430 \u0441\u0430\u0439\u0442. \u0414\u043b\u044f \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0430 \u0434\u0440\u0443\u0433\u043e\u0433\u043e \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0430 \u043d\u0443\u0436\u043d\u043e \u043b\u0438\u0431\u043e \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0432 \u043d\u043e\u0432\u043e\u043c \u043e\u043a\u043d\u0435 \u0438\u043d\u043a\u043e\u0433\u043d\u0438\u0442\u043e, \u043b\u0438\u0431\u043e \u043e\u0447\u0438\u0441\u0442\u0438\u0442\u044c \u043a\u0443\u043a\u0438 \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443.\" width=\"1920\" height=\"1080\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/141\/b77\/47c\/141b7747cfdbaf8dbdcd5b06289f6cb4.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/141\/b77\/47c\/141b7747cfdbaf8dbdcd5b06289f6cb4.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>\u042d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442\u0441\u044f \u0432 \u043a\u0443\u043a\u0430\u0445 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e\u0441\u0442\u0438 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0430 \u043f\u0440\u0438 \u0437\u0430\u0445\u043e\u0434\u0435 \u043d\u0430 \u0441\u0430\u0439\u0442. \u0414\u043b\u044f \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0430 \u0434\u0440\u0443\u0433\u043e\u0433\u043e \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0430 \u043d\u0443\u0436\u043d\u043e \u043b\u0438\u0431\u043e \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0432 \u043d\u043e\u0432\u043e\u043c \u043e\u043a\u043d\u0435 \u0438\u043d\u043a\u043e\u0433\u043d\u0438\u0442\u043e, \u043b\u0438\u0431\u043e \u043e\u0447\u0438\u0441\u0442\u0438\u0442\u044c \u043a\u0443\u043a\u0438 \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443.<\/figcaption><\/div>\n<\/figure>\n<p>\u0421\u043a\u0440\u0438\u043f\u0442 <a href=\"https:\/\/github.com\/andrewbrdk\/AB-Testing-from-Scratch\/blob\/main\/simulate_visits.py\" rel=\"noopener noreferrer nofollow\"><code>simulate_visits.py<\/code><\/a> \u0438\u043c\u0438\u0442\u0438\u0440\u0443\u0435\u0442 \u0437\u0430\u0445\u043e\u0434\u044b \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443. \u0420\u0430\u0441\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u043f\u043e \u0433\u0440\u0443\u043f\u043f\u0430\u043c \u0431\u043b\u0438\u0437\u043a\u043e \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u043e\u043c\u0443 50\/50.<\/p>\n<pre><code class=\"bash\">&gt; python simulate_visits.py -n 1000  Moon\/Mars Exp Split: Mars: 488 visits (48.80%), Exact 50.00% Moon: 512 visits (51.20%), Exact 50.00%<\/code><\/pre>\n<p><a class=\"anchor\" name=\"%D0%A5%D1%8D%D1%88%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5\" id=\"\u0425\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\"><\/a><\/p>\n<h2>2. \u0425\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435<\/h2>\n<p>\u041a\u0430\u0436\u0434\u043e\u043c\u0443 \u043f\u043e\u0441\u0435\u0442\u0438\u0442\u0435\u043b\u044e \u043f\u0440\u0438\u0441\u0432\u0430\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0439 <code>device_id<\/code> \u0438 \u0437\u0430\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0432 \u043a\u0443\u043a\u0438. \u042d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430 \u0432\u044b\u0447\u0438\u0441\u043b\u044f\u0435\u0442\u0441\u044f \u043a\u0430\u043a <code>hash(device_id || experiment_name) % 2<\/code>.<\/p>\n<pre><code class=\"bash\">python 2_hash.py<\/code><\/pre>\n<p>\u042d\u043a\u0441\u043f: <a href=\"http:\/\/127.0.0.1:5000\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000<\/a><\/p>\n<pre><code class=\"python\">from flask import Flask, render_template_string, request, make_response import uuid import hashlib  app = Flask(__name__)  TEMPLATE = \"\"\" &lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt;     &lt;title&gt;A\/B Test&lt;\/title&gt;     &lt;link rel=\"stylesheet\" href=\"{{ url_for('static', filename='banners.css') }}\"&gt; &lt;\/head&gt; &lt;body&gt;     {% if variant == 'Moon' %}         &lt;div class=\"banner\" style=\"background-image: url('{{ url_for('static', filename='.\/moon.jpg') }}');\"&gt;             &lt;h1&gt;Walk on the Moon&lt;\/h1&gt;             &lt;div class=\"vspacer\"&gt;&lt;\/div&gt;             &lt;p&gt;Be one of the first tourists to set foot on the lunar surface. Your journey to another world starts here.&lt;\/p&gt;             &lt;button onclick=\"console.log('Click Moon')\"&gt;Reserve Your Spot&lt;\/button&gt;         &lt;\/div&gt;     {% else %}         &lt;div class=\"banner\" style=\"background-image: url('{{ url_for('static', filename='.\/mars.jpg') }}');\"&gt;             &lt;h1&gt;Journey to Mars&lt;\/h1&gt;             &lt;div class=\"vspacer\"&gt;&lt;\/div&gt;             &lt;p&gt;Be among the first humans to set foot on the Red Planet. Experience the adventure of a lifetime.&lt;\/p&gt;             &lt;button onclick=\"console.log('Click Mars')\"&gt;Reserve Your Spot&lt;\/button&gt;         &lt;\/div&gt;     {% endif %} &lt;\/body&gt; &lt;\/html&gt; \"\"\"  EXPERIMENT_NAME = \"moon_mars\"  def assign_group(device_id: str, experiment: str) -&gt; str:     key = f\"{device_id}:{experiment}\"     hash_bytes = hashlib.sha256(key.encode()).digest()     hash_int = int.from_bytes(hash_bytes, 'big')     return 'Moon' if hash_int % 2 == 0 else 'Mars'  @app.route('\/') def index():     device_id = request.cookies.get(\"device_id\")     if not device_id:         device_id = str(uuid.uuid4())     variant = assign_group(device_id, EXPERIMENT_NAME)     response = make_response(render_template_string(TEMPLATE, variant=variant))     response.set_cookie(\"device_id\", device_id, max_age=60*60*24*365)     return response  if __name__ == '__main__':     app.run(debug=True)<\/code><\/pre>\n<ul>\n<li>\n<p><code>device_id = str(uuid.uuid4())<\/code> &#8212; \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u0435\u0442 \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0439 ID \u043d\u043e\u0432\u044b\u043c \u043f\u043e\u0441\u0435\u0442\u0438\u0442\u0435\u043b\u044f\u043c.<\/p>\n<\/li>\n<li>\n<p><code>variant = assign_group(device_id, EXPERIMENT_NAME)<\/code> &#8212; \u0432\u044b\u0447\u0438\u0441\u043b\u044f\u0435\u0442 \u0433\u0440\u0443\u043f\u043f\u0443.<\/p>\n<\/li>\n<li>\n<p><code>key = f\"{device_id}:{experiment}\"<\/code> &#8212; \u043e\u0431\u044a\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 ID \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430 \u0434\u043b\u044f \u0432\u044b\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044f \u0433\u0440\u0443\u043f\u043f\u044b.<\/p>\n<\/li>\n<li>\n<p><code>response.set_cookie(\"device_id\", device_id, max_age=60*60*24*365)<\/code> &#8212; \u0437\u0430\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442 <code>device_id<\/code> \u0432 \u043a\u0443\u043a\u0438.<\/p>\n<\/li>\n<\/ul>\n<p>\u0420\u0430\u0441\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u043f\u043e \u0433\u0440\u0443\u043f\u043f\u0430\u043c \u0440\u0430\u0432\u043d\u043e\u043c\u0435\u0440\u043d\u043e\u0435.<\/p>\n<pre><code class=\"bash\">&gt; python simulate_visits.py -n 1000  Moon\/Mars Exp Split: Mars: 507 visits (50.70%), Exact 50.00% Moon: 493 visits (49.30%), Exact 50.00%<\/code><\/pre>\n<p><a class=\"anchor\" name=\"%D0%A4%D1%80%D0%BE%D0%BD%D1%82%D1%8D%D0%BD%D0%B4\" id=\"\u0424\u0440\u043e\u043d\u0442\u044d\u043d\u0434\"><\/a><\/p>\n<h2>3. \u0424\u0440\u043e\u043d\u0442\u044d\u043d\u0434<\/h2>\n<p>\u0424\u0440\u043e\u043d\u0442\u044d\u043d\u0434 \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u043e\u0431\u0435 \u0432\u0435\u0440\u0441\u0438\u0438 \u0438 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u0442 \u043d\u0443\u0436\u043d\u044b\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442. \u0413\u0440\u0443\u043f\u043f\u0430 \u0432\u044b\u0447\u0438\u0441\u043b\u044f\u0435\u0442\u0441\u044f \u043d\u0430 \u0431\u044d\u043a\u044d\u043d\u0434\u0435 \u0438 \u043f\u0435\u0440\u0435\u0434\u0430\u0451\u0442\u0441\u044f \u0432 \u043a\u0443\u043a\u0435 <code>\"exp_group\"<\/code>. \u0421 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0445\u0435\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0433\u0440\u0443\u043f\u043f\u0443 \u043c\u043e\u0436\u043d\u043e \u0432\u044b\u0447\u0438\u0441\u043b\u044f\u0442\u044c \u043d\u0430 \u0444\u0440\u043e\u043d\u0442\u044d\u043d\u0434\u0435 \u043f\u0440\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e\u043c <code>device_id<\/code>.<\/p>\n<pre><code class=\"bash\">python 3_frontend.py<\/code><\/pre>\n<p>\u042d\u043a\u0441\u043f: <a href=\"http:\/\/127.0.0.1:5000\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000<\/a><\/p>\n<pre><code class=\"python\">from flask import Flask, request, make_response, render_template_string import uuid import hashlib  app = Flask(__name__)  TEMPLATE = \"\"\" &lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt;     &lt;title&gt;A\/B Test&lt;\/title&gt;     &lt;link rel=\"stylesheet\" href=\"{{ url_for('static', filename='banners.css') }}\"&gt; &lt;\/head&gt; &lt;body&gt;     &lt;div id=\"variant-container\"&gt;Loading...&lt;\/div&gt;      &lt;script&gt;         function getCookie(name) {             const value = `; ${document.cookie}`;             const parts = value.split(`; ${name}=`);             if (parts.length === 2) return parts.pop().split(';').shift();         }          const expGroup = getCookie(\"exp_group\");         const container = document.getElementById(\"variant-container\");          if (expGroup === \"Moon\") {             container.innerHTML = `                 &lt;div class=\"banner\" style=\"background-image: url('{{ url_for('static', filename='.\/moon.jpg') }}');\"&gt;                     &lt;h1&gt;Walk on the Moon&lt;\/h1&gt;                     &lt;div class=\"vspacer\"&gt;&lt;\/div&gt;                     &lt;p&gt;Be one of the first tourists to set foot on the lunar surface. Your journey to another world starts here.&lt;\/p&gt;                     &lt;button onclick=\"console.log('Click Moon')\"&gt;Reserve Your Spot&lt;\/button&gt;                 &lt;\/div&gt;             `;         } else {             container.innerHTML = `                 &lt;div class=\"banner\" style=\"background-image: url('{{ url_for('static', filename='.\/mars.jpg') }}');\"&gt;                     &lt;h1&gt;Journey to Mars&lt;\/h1&gt;                     &lt;div class=\"vspacer\"&gt;&lt;\/div&gt;                     &lt;p&gt;Be among the first humans to set foot on the Red Planet. Experience the adventure of a lifetime.&lt;\/p&gt;                     &lt;button onclick=\"console.log('Click Mars')\"&gt;Reserve Your Spot&lt;\/button&gt;                 &lt;\/div&gt;             `;         }     &lt;\/script&gt; &lt;\/body&gt; &lt;\/html&gt; \"\"\"  EXPERIMENT_NAME = \"moon_mars\"  def assign_group(device_id: str, experiment: str) -&gt; str:     key = f\"{device_id}:{experiment}\"     hash_bytes = hashlib.sha256(key.encode()).digest()     hash_int = int.from_bytes(hash_bytes, 'big')     return 'Moon' if hash_int % 2 == 0 else 'Mars'  @app.route('\/') def index():     device_id = request.cookies.get(\"device_id\")     if not device_id:         device_id = str(uuid.uuid4())     variant = assign_group(device_id, EXPERIMENT_NAME)     response = make_response(render_template_string(TEMPLATE))     response.set_cookie(\"device_id\", device_id, max_age=60*60*24*365)     response.set_cookie(\"exp_group\", variant, max_age=60*60*24*365)     return response  if __name__ == '__main__':     app.run(debug=True)<\/code><\/pre>\n<ul>\n<li>\n<p><code>&lt;div id=\"variant-container\"&gt;Loading...&lt;\/div&gt;<\/code> &#8212; \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430.<\/p>\n<\/li>\n<li>\n<p><code>const expGroup = getCookie(\"exp_group\");<\/code> &#8212; \u0447\u0442\u0435\u043d\u0438\u0435 \u0433\u0440\u0443\u043f\u043f\u044b \u0438\u0437 \u043a\u0443\u043a.<\/p>\n<\/li>\n<li>\n<p><code>if (expGroup === \"Moon\") { container.innerHTML = ... }<\/code> &#8212; \u0437\u0430\u043c\u0435\u043d\u0430 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u043e\u043c, \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u043c \u0433\u0440\u0443\u043f\u043f\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f.<\/p>\n<\/li>\n<\/ul>\n<p>\u0414\u0435\u043b\u0435\u043d\u0438\u0435 \u0442\u0440\u0430\u0444\u0438\u043a\u0430 \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e\u0435.<\/p>\n<pre><code class=\"bash\">&gt; python simulate_visits.py -n 1000  Moon\/Mars Exp Split: Mars: 502 visits (50.20%), Exact 50.00% Moon: 498 visits (49.80%), Exact 50.00%<\/code><\/pre>\n<p><a class=\"anchor\" name=\"%D0%A1%D0%BE%D0%B1%D1%8B%D1%82%D0%B8%D1%8F\" id=\"\u0421\u043e\u0431\u044b\u0442\u0438\u044f\"><\/a><\/p>\n<h2>4. \u0421\u043e\u0431\u044b\u0442\u0438\u044f<\/h2>\n<p>\u041f\u0440\u0438 \u0437\u0430\u0445\u043e\u0434\u0435 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0438\u043b\u0438 \u043d\u0430\u0436\u0430\u0442\u0438\u0438 \u043a\u043d\u043e\u043f\u043a\u0438 \u0431\u0430\u043d\u043d\u0435\u0440\u0430 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0441\u043e\u0431\u044b\u0442\u0438\u044f <code>pageview<\/code> \u0438 <code>button_click<\/code>. \u0421\u043e\u0431\u044b\u0442\u0438\u044f &#8212; \u044d\u0442\u043e JSON&#8217;\u044b \u0441 <code>device_id<\/code>, <code>event_name<\/code>, \u043c\u043e\u043c\u0435\u043d\u0442\u043e\u043c \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0438 \u0434\u0440\u0443\u0433\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0435\u0439. \u0414\u0430\u043d\u043d\u044b\u0435 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u043d\u0430 \u044d\u043d\u0434\u043f\u043e\u0438\u043d\u0442 <code>\/events<\/code>, \u0432 \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0445 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0445 \u0447\u0430\u0441\u0442\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u0441\u0435\u0440\u0432\u0438\u0441.<\/p>\n<pre><code class=\"bash\">python 4_events.py<\/code><\/pre>\n<p>\u042d\u043a\u0441\u043f: <a href=\"http:\/\/127.0.0.1:5000%EF%BF%BC\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000<br \/><\/a>\u0421\u043e\u0431\u044b\u0442\u0438\u044f: <a href=\"http:\/\/127.0.0.1:5000\/events\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000\/events<\/a><\/p>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/b87\/495\/1e2\/b874951e222c7ef2a01e865c5029191f.png\" alt=\"\u0421\u043e\u0431\u044b\u0442\u0438\u044f.\" title=\"\u0421\u043e\u0431\u044b\u0442\u0438\u044f.\" width=\"1920\" height=\"1080\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/b87\/495\/1e2\/b874951e222c7ef2a01e865c5029191f.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/b87\/495\/1e2\/b874951e222c7ef2a01e865c5029191f.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>\u0421\u043e\u0431\u044b\u0442\u0438\u044f.<\/figcaption><\/div>\n<\/figure>\n<pre><code class=\"python\">from flask import Flask, request, make_response, render_template_string, jsonify import uuid import hashlib  app = Flask(__name__)  TEMPLATE = \"\"\" &lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt;     &lt;title&gt;A\/B Test&lt;\/title&gt;     &lt;link rel=\"stylesheet\" href=\"{{ url_for('static', filename='banners.css') }}\"&gt; &lt;\/head&gt; &lt;body&gt;     &lt;div id=\"variant-container\"&gt;Loading...&lt;\/div&gt;      &lt;script&gt;         async function sendEvent(eventName, params = {}) {             let ts = new Date().toISOString();             await fetch('\/events', {                 method: 'POST',                 headers: { 'Content-Type': 'application\/json' },                 body: JSON.stringify({                     ts: ts,                     device_id: deviceId,                     source: 'client',                     event: eventName,                     exp_group: expGroup,                     params: params                 })             });         }          function getCookie(name) {             const value = `; ${document.cookie}`;             const parts = value.split(`; ${name}=`);             if (parts.length === 2) return parts.pop().split(';').shift();         }          const deviceId = getCookie(\"device_id\");         const expGroup = getCookie(\"exp_group\");         const container = document.getElementById(\"variant-container\");          if (expGroup === \"Moon\") {             container.innerHTML = `                 &lt;div class=\"banner\" style=\"background-image: url('{{ url_for('static', filename='.\/moon.jpg') }}');\"&gt;                     &lt;h1&gt;Walk on the Moon&lt;\/h1&gt;                     &lt;div class=\"vspacer\"&gt;&lt;\/div&gt;                     &lt;p&gt;Be one of the first tourists to set foot on the lunar surface. Your journey to another world starts here.&lt;\/p&gt;                     &lt;button onclick=\"sendEvent('button_click', { btn_type: 'Moon' })\"&gt;Reserve Your Spot&lt;\/button&gt;                 &lt;\/div&gt;             `;         } else {             container.innerHTML = `                 &lt;div class=\"banner\" style=\"background-image: url('{{ url_for('static', filename='.\/mars.jpg') }}');\"&gt;                     &lt;h1&gt;Journey to Mars&lt;\/h1&gt;                     &lt;div class=\"vspacer\"&gt;&lt;\/div&gt;                     &lt;p&gt;Be among the first humans to set foot on the Red Planet. Experience the adventure of a lifetime.&lt;\/p&gt;                     &lt;button onclick=\"sendEvent('button_click', { btn_type: 'Mars' })\"&gt;Reserve Your Spot&lt;\/button&gt;                 &lt;\/div&gt;             `;         }          sendEvent(\"pageview\", {});     &lt;\/script&gt; &lt;\/body&gt; &lt;\/html&gt; \"\"\"  EXPERIMENT_NAME = \"moon_mars\"  def assign_group(device_id: str, experiment: str) -&gt; str:     key = f\"{device_id}:{experiment}\"     hash_bytes = hashlib.sha256(key.encode()).digest()     hash_int = int.from_bytes(hash_bytes, 'big')     return 'Moon' if hash_int % 2 == 0 else 'Mars'  @app.route('\/') def index():     device_id = request.cookies.get(\"device_id\")     if not device_id:         device_id = str(uuid.uuid4())     variant = assign_group(device_id, EXPERIMENT_NAME)     response = make_response(render_template_string(TEMPLATE))     response.set_cookie(\"device_id\", device_id, max_age=60*60*24*365)     response.set_cookie(\"exp_group\", variant, max_age=60*60*24*365)     return response  EVENTS = []  @app.route('\/events', methods=['GET', 'POST']) def events():     if request.method == 'POST':         data = request.json         EVENTS.append(data)         return jsonify({\"status\": \"ok\"})     else:         return jsonify(EVENTS)  if __name__ == '__main__':     app.run(debug=True)<\/code><\/pre>\n<ul>\n<li>\n<p><code>async function sendEvent(eventName, params = {})<\/code> &#8212; \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u0430\u043d\u0430\u043b\u0438\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f. \u041f\u043e\u043b\u0435 <code>params<\/code> \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u043f\u0435\u0446\u0438\u0444\u0438\u0447\u0435\u0441\u043a\u0443\u044e \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0442\u0438\u043f\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e.<\/p>\n<\/li>\n<li>\n<p><code>sendEvent(\"button_click\", ...)<\/code>  &#8212; \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u0441\u043e\u0431\u044b\u0442\u0438\u0435 <code>button_click<\/code>.<\/p>\n<\/li>\n<li>\n<p><code>sendEvent(\"pageview\", {});<\/code> &#8212; \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u0441\u043e\u0431\u044b\u0442\u0438\u0435 <code>pageview<\/code>.<\/p>\n<\/li>\n<li>\n<p><code>EVENTS = []<\/code> &#8212; \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f \u0445\u0440\u0430\u043d\u044f\u0442\u0441\u044f \u0432 \u0441\u043f\u0438\u0441\u043a\u0435 <code>EVENTS<\/code> .<\/p>\n<\/li>\n<li>\n<p><code>@app.route('\/events', methods=['GET', 'POST'])<\/code> &#8212; \u044d\u043d\u0434\u043f\u043e\u0438\u043d\u0442 \u0434\u043b\u044f \u0441\u0431\u043e\u0440\u0430 \u0441\u043e\u0431\u044b\u0442\u0438\u0439.<\/p>\n<\/li>\n<\/ul>\n<p>\u0412 <code>simulate_visits.py<\/code> \u0438\u043c\u0438\u0442\u0438\u0440\u0443\u044e\u0442\u0441\u044f \u0437\u0430\u0445\u043e\u0434\u044b \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0438 \u043d\u0430\u0436\u0430\u0442\u0438\u044f \u043a\u043d\u043e\u043f\u043e\u043a. \u0412\u0435\u0440\u043e\u044f\u0442\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0436\u0430\u0442\u0438\u044f \u043a\u043d\u043e\u043f\u043a\u0438 \u0432 \u0433\u0440\u0443\u043f\u043f\u0430\u0445 \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f <code>CLICK_PROBS = {'Moon': 0.1, 'Mars': 0.2}<\/code>. \u041f\u0440\u0438 \u0437\u0430\u0445\u043e\u0434\u0435 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0438\u043b\u0438 \u043d\u0430\u0436\u0430\u0442\u0438\u0438 \u043a\u043d\u043e\u043f\u043a\u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0430\u043d\u0430\u043b\u0438\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f. \u041a\u043e\u043d\u0432\u0435\u0440\u0441\u0438\u0438 \u043f\u043e \u044d\u0442\u0438\u043c \u0441\u043e\u0431\u044b\u0442\u0438\u044f\u043c \u0431\u043b\u0438\u0437\u043a\u0438 <code>CLICK_PROBS<\/code>.<\/p>\n<pre><code class=\"bash\">&gt; python simulate_visits.py -n 1000  Moon\/Mars Exp Split: Mars: 490 visits (49.00%), Exact 50.00% Moon: 510 visits (51.00%), Exact 50.00%  Moon\/Mars Exp events: Mars: 490 visits, 95 clicks, Conv=19.39 +- 3.57%, Exact: 20.00% Moon: 510 visits, 51 clicks, Conv=10.00 +- 2.66%, Exact: 10.00%<\/code><\/pre>\n<p><a class=\"anchor\" name=\"%D0%9A%D0%BE%D0%BD%D1%84%D0%B8%D0%B3\" id=\"\u041a\u043e\u043d\u0444\u0438\u0433\"><\/a><\/p>\n<h2>5. \u041a\u043e\u043d\u0444\u0438\u0433<\/h2>\n<p>\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430 \u0441\u043e\u0431\u0440\u0430\u043d\u044b \u0432 \u043a\u043e\u043d\u0444\u0438\u0433. \u041f\u0440\u043e\u043f\u0438\u0441\u0430\u043d\u044b \u0433\u0440\u0443\u043f\u043f\u044b \u0438 \u0432\u0435\u0441\u0430, \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430, \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u0441\u0442\u0430\u0442\u0443\u0441. \u041a\u043e\u043d\u0444\u0438\u0433 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d \u043d\u0430 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u043e\u0432 \u0441 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u043b\u044c\u043d\u044b\u043c \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e\u043c \u0433\u0440\u0443\u043f\u043f \u0438 \u0434\u0435\u043b\u0435\u043d\u0438\u0435\u043c \u0442\u0440\u0430\u0444\u0438\u043a\u0430.<\/p>\n<pre><code>python 5_config.py<\/code><\/pre>\n<p>\u042d\u043a\u0441\u043f: <a href=\"http:\/\/127.0.0.1:5000%EF%BF%BC\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000<br \/><\/a>\u0421\u043e\u0431\u044b\u0438\u0442\u044f: <a href=\"http:\/\/127.0.0.1:5000\/events%EF%BF%BC\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000\/events<br \/><\/a>\u042d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u044b: <a href=\"http:\/\/127.0.0.1:5000\/api\/experiments%EF%BF%BC\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000\/api\/experiments<br \/><\/a>\u0413\u0440\u0443\u043f\u043f\u044b: <a href=\"http:\/\/127.0.0.1:5000\/api\/expgroups\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000\/api\/expgroups<\/a><\/p>\n<pre><code class=\"python\"># ...  INDEX_TEMPLATE = \"\"\" \/\/ ... &lt;body&gt;     &lt;div id=\"variant-container\"&gt;Loading...&lt;\/div&gt;      &lt;script&gt;         \/\/ ...          async function getExpGroups(deviceId) {             const res = await fetch(`\/api\/expgroups?device_id=${deviceId}`);             return await res.json();         }          async function renderPage() {             const experiments = await getExpGroups(deviceId);             const exp = experiments[\"moon_mars\"];             const container = document.getElementById(\"variant-container\");             if (exp.group === \"Moon\") {                 container.innerHTML = `                     &lt;div class=\"banner\" style=\"background-image: url('{{ url_for('static', filename='.\/moon.jpg') }}');\"&gt;                         &lt;h1&gt;Walk on the Moon&lt;\/h1&gt;                         &lt;div class=\"vspacer\"&gt;&lt;\/div&gt;                         &lt;p&gt;Be one of the first tourists to set foot on the lunar surface. Your journey to another world starts here.&lt;\/p&gt;                         &lt;button onclick=\"sendEvent('button_click', { btn_type: 'Moon' })\"&gt;Reserve Your Spot&lt;\/button&gt;                     &lt;\/div&gt;                 `;             } else {                 container.innerHTML = `                     &lt;div class=\"banner\" style=\"background-image: url('{{ url_for('static', filename='.\/mars.jpg') }}');\"&gt;                         &lt;h1&gt;Journey to Mars&lt;\/h1&gt;                         &lt;div class=\"vspacer\"&gt;&lt;\/div&gt;                         &lt;p&gt;Be among the first humans to set foot on the Red Planet. Experience the adventure of a lifetime.&lt;\/p&gt;                         &lt;button onclick=\"sendEvent('button_click', { btn_type: 'Mars' })\"&gt;Reserve Your Spot&lt;\/button&gt;                     &lt;\/div&gt;                 `;             }         }          const deviceId = getCookie(\"device_id\");         sendEvent(\"pageview\", {});         renderPage();     &lt;\/script&gt; &lt;\/body&gt; &lt;\/html&gt; \"\"\"  @app.route('\/') def index():     device_id = request.cookies.get(\"device_id\")     if not device_id:         device_id = str(uuid.uuid4())     response = make_response(render_template_string(INDEX_TEMPLATE))     response.set_cookie(\"device_id\", device_id, max_age=60*60*24*365)     return response  # ...  EXPERIMENTS = {     \"moon_mars\": {         \"groups\": {'Moon': 50, 'Mars': 50},         \"fallback\": \"Moon\",         \"state\": \"active\",     } }  @app.route('\/api\/experiments') def api_experiments():     return jsonify(EXPERIMENTS)  @app.route('\/api\/expgroups') def api_expgroups():     device_id = request.args.get(\"device_id\")     result = {}     for exp_name, info in EXPERIMENTS.items():         group = assign_group(device_id, exp_name) if device_id else \"\"         result[exp_name] = {             \"state\": info[\"state\"],             \"fallback\": info[\"fallback\"],             \"group\": group         }     if device_id:         post_event(\"exp_groups\", device_id, result)     return jsonify(result)  def assign_group(device_id: str, experiment: str) -&gt; str:     groups = EXPERIMENTS[experiment][\"groups\"]     total_parts = sum(groups.values())     key = f\"{device_id}:{experiment}\"     hash_bytes = hashlib.sha256(key.encode()).digest()     hash_int = int.from_bytes(hash_bytes, 'big')     hash_mod = hash_int % total_parts     c = 0     chosen = EXPERIMENTS[experiment][\"fallback\"]     for group_name, weight in sorted(groups.items()):         c += weight         if hash_mod &lt; c:             chosen = group_name             break     return chosen  def post_event(event_name: str, device_id: str, params: dict):     payload = {         \"ts\": datetime.utcnow().isoformat(),         \"deviceId\": device_id,         \"source\": 'backend',         \"event\": event_name,         \"params\": params     }     with app.test_request_context(\"\/events\", method=\"POST\", json=payload):         return events()  if __name__ == '__main__':     app.run(debug=True)<\/code><\/pre>\n<ul>\n<li>\n<p><code>async function getExpGroups(deviceId)<\/code> &#8212; \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u0433\u0440\u0443\u043f\u043f\u044b \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u0430. <\/p>\n<\/li>\n<li>\n<p><code>if (exp.group === \"Moon\") { ... }<\/code> &#8212; \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u0442 \u043d\u0443\u0436\u043d\u044b\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442.<\/p>\n<\/li>\n<li>\n<p><code>def index()<\/code> &#8212; \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0432\u044b\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u043a\u0443\u043a\u0443 &#171;exp_group&#187;.<\/p>\n<\/li>\n<li>\n<p><code>EXPERIMENTS<\/code> &#8212; \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u043e\u0432.<\/p>\n<\/li>\n<li>\n<p><code>@app.route('\/api\/experiments')<\/code> &#8212; \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e\u0431 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430\u0445.<\/p>\n<\/li>\n<li>\n<p><code>@app.route('\/api\/expgroups')<\/code> &#8212; \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0433\u0440\u0443\u043f\u043f\u044b \u0437\u0430\u0434\u0430\u043d\u043d\u043e\u0433\u043e <code>device_id<\/code>.<\/p>\n<\/li>\n<li>\n<p><code>hash_mod = hash_int % total_parts<\/code> &#8212; \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442\u0441\u044f \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u044b \u0441 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u043b\u044c\u043d\u044b\u043c \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e\u043c \u0433\u0440\u0443\u043f\u043f \u0438 \u0434\u0435\u043b\u0435\u043d\u0438\u0435\u043c \u0442\u0440\u0430\u0444\u0438\u043a\u0430.<\/p>\n<\/li>\n<li>\n<p><code>post_event(\"exp_groups\", device_id, result)<\/code> &#8212; \u0431\u044d\u043a\u0435\u043d\u0434 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u0430\u043d\u0430\u043b\u0438\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u043f\u0440\u0438 \u043f\u0440\u0438\u0441\u0432\u043e\u0435\u043d\u0438\u0438 \u0433\u0440\u0443\u043f\u043f.<\/p>\n<\/li>\n<\/ul>\n<p>\u0414\u0435\u043b\u0435\u043d\u0438\u0435 \u0442\u0440\u0430\u0444\u0438\u043a\u0430 \u0438 \u043a\u043e\u043d\u0432\u0435\u0440\u0441\u0438\u0438 \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u044b.<\/p>\n<pre><code class=\"bash\">&gt; python simulate_visits.py -n 1000  Moon\/Mars Exp Split: Mars: 492 visits (49.20%), Exact 50.00% Moon: 508 visits (50.80%), Exact 50.00%  Moon\/Mars Exp events: Mars: 492 visits, 111 clicks, Conv=22.56 +- 3.77%, Exact: 20.00% Moon: 508 visits, 55 clicks, Conv=10.83 +- 2.76%, Exact: 10.00%<\/code><\/pre>\n<p><a class=\"anchor\" name=\"%D0%94%D0%B2%D0%B0-%D1%8D%D0%BA%D1%81%D0%BF%D0%B5%D1%80%D0%B8%D0%BC%D0%B5%D0%BD%D1%82%D0%B0\" id=\"\u0414\u0432\u0430-\u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430\"><\/a><\/p>\n<h2>6. \u0414\u0432\u0430 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430<\/h2>\n<p>\u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d \u0432\u0442\u043e\u0440\u043e\u0439 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442 \u0441 \u0434\u0432\u0443\u043c\u044f \u0433\u0440\u0443\u043f\u043f\u0430\u043c\u0438, \u0447\u0442\u043e \u0434\u0430\u0435\u0442 \u0447\u0435\u0442\u044b\u0440\u0435 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b. \u0414\u0435\u043b\u0435\u043d\u0438\u0435 \u043f\u043e \u0433\u0440\u0443\u043f\u043f\u0430\u043c \u0432 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430\u0445 \u043d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e \u0442.\u043a. \u0432 \u0445\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438 \u043f\u0440\u0438 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0438 \u0433\u0440\u0443\u043f\u043f\u044b <code>hash(device_id || experiment_name) % n_groups<\/code> \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0439<code>experiment_name<\/code>. \u041e\u0431\u0430 \u044d\u043d\u0434\u043f\u043e\u0438\u043d\u0442\u0430 <code>api\/experiments<\/code> \u0438 <code>api\/expgroups<\/code> \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u043e\u0432.<\/p>\n<pre><code class=\"bash\">python 6_multiexps.py<\/code><\/pre>\n<p>\u042d\u043a\u0441\u043f: <a href=\"http:\/\/127.0.0.1:5000%EF%BF%BC\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000<br \/><\/a>\u0421\u043e\u0431\u044b\u0442\u0438\u044f: <a href=\"http:\/\/127.0.0.1:5000\/events%EF%BF%BC\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000\/events<br \/><\/a>\u042d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u044b: <a href=\"http:\/\/127.0.0.1:5000\/api\/experiments%EF%BF%BC\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000\/api\/experiments<br \/><\/a>\u0413\u0440\u0443\u043f\u043f\u044b: <a href=\"http:\/\/127.0.0.1:5000\/api\/expgroups\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000\/api\/expgroups<\/a><\/p>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/9c5\/d83\/571\/9c5d835716f4ba6ecf9faae7efaef483.png\" alt=\"\u0427\u0435\u0442\u044b\u0440\u0435 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b - \u0434\u0432\u0430 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430 \u043f\u043e \u0434\u0432\u0435 \u0433\u0440\u0443\u043f\u043f\u044b \u0432 \u043a\u0430\u0436\u0434\u043e\u043c.\" title=\"\u0427\u0435\u0442\u044b\u0440\u0435 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b - \u0434\u0432\u0430 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430 \u043f\u043e \u0434\u0432\u0435 \u0433\u0440\u0443\u043f\u043f\u044b \u0432 \u043a\u0430\u0436\u0434\u043e\u043c.\" width=\"1920\" height=\"1080\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/9c5\/d83\/571\/9c5d835716f4ba6ecf9faae7efaef483.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/9c5\/d83\/571\/9c5d835716f4ba6ecf9faae7efaef483.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>\u0427\u0435\u0442\u044b\u0440\u0435 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b &#8212; \u0434\u0432\u0430 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430 \u043f\u043e \u0434\u0432\u0435 \u0433\u0440\u0443\u043f\u043f\u044b \u0432 \u043a\u0430\u0436\u0434\u043e\u043c.<\/figcaption><\/div>\n<\/figure>\n<pre><code class=\"python\"># ...  INDEX_TEMPLATE = \"\"\" \/\/ ... &lt;body&gt;     &lt;div id=\"variant-container\"&gt;Loading...&lt;\/div&gt;      &lt;script&gt;         \/\/ ...          async function getExpGroups(deviceId) {             const res = await fetch(`\/api\/expgroups?device_id=${deviceId}`);             return await res.json();         }          async function renderPage() {             const experiments = await getExpGroups(deviceId);             let exp = experiments[\"moon_mars\"];             let moon_mars_group = exp.group;             exp = experiments[\"white_gold_btn\"];             let white_gold_group = exp.group;             const container = document.getElementById(\"variant-container\");             let btn_cls = white_gold_group === \"White\" ? 'class=\"white\"' : 'class=\"gold\"';             if (moon_mars_group === \"Moon\") {                 container.innerHTML = `                     &lt;div class=\"banner\" style=\"background-image: url('{{ url_for('static', filename='.\/moon.jpg') }}');\"&gt;                         &lt;h1&gt;Walk on the Moon&lt;\/h1&gt;                         &lt;div class=\"vspacer\"&gt;&lt;\/div&gt;                         &lt;p&gt;Be one of the first tourists to set foot on the lunar surface. Your journey to another world starts here.&lt;\/p&gt;                         &lt;button ${btn_cls} onclick=\"sendEvent('button_click', { btn_type: 'Moon' })\"&gt;Reserve Your Spot&lt;\/button&gt;                     &lt;\/div&gt;                 `;             } else {                 container.innerHTML = `                     &lt;div class=\"banner\" style=\"background-image: url('{{ url_for('static', filename='.\/mars.jpg') }}');\"&gt;                         &lt;h1&gt;Journey to Mars&lt;\/h1&gt;                         &lt;div class=\"vspacer\"&gt;&lt;\/div&gt;                         &lt;p&gt;Be among the first humans to set foot on the Red Planet. Experience the adventure of a lifetime.&lt;\/p&gt;                         &lt;button ${btn_cls} onclick=\"sendEvent('button_click', { btn_type: 'Mars' })\"&gt;Reserve Your Spot&lt;\/button&gt;                     &lt;\/div&gt;                 `;             }         }          \/\/ ...     &lt;\/script&gt; &lt;\/body&gt; &lt;\/html&gt; \"\"\"  # ...  EXPERIMENTS = {     \"moon_mars\": {         \"groups\": {'Moon': 50, 'Mars': 50},         \"fallback\": \"Moon\",         \"state\": \"active\"     },     \"white_gold_btn\": {         \"groups\": {'White': 50, 'Gold': 50},         \"fallback\": \"White\",         \"state\": \"active\"     } }  @app.route('\/api\/expgroups') def api_expgroups():     device_id = request.args.get(\"device_id\")     result = {}     for exp_name, info in EXPERIMENTS.items():         group = assign_group(device_id, exp_name) if device_id else \"\"         result[exp_name] = {             \"state\": info[\"state\"],             \"fallback\": info[\"fallback\"],             \"group\": group         }     if device_id:         post_event(\"exp_groups\", device_id, result)     return jsonify(result)  def assign_group(device_id: str, experiment: str) -&gt; str:     groups = EXPERIMENTS[experiment][\"groups\"]     total_parts = sum(groups.values())     key = f\"{device_id}:{experiment}\"     hash_bytes = hashlib.sha256(key.encode()).digest()     hash_int = int.from_bytes(hash_bytes, 'big')     hash_mod = hash_int % total_parts     c = 0     chosen = EXPERIMENTS[experiment][\"fallback\"]     for group_name, weight in sorted(groups.items()):         c += weight         if hash_mod &lt; c:             chosen = group_name             break     return chosen  # ...<\/code><\/pre>\n<ul>\n<li>\n<p><code>async function getExpGroups(deviceId)<\/code> &#8212; \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u0433\u0440\u0443\u043f\u043f\u044b \u043e\u0431\u043e\u0438\u0445 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u043e\u0432.<\/p>\n<\/li>\n<li>\n<p><code>let btn_cls = white_gold_group === \"White\" ? 'class=\"white\"' : 'class=\"gold\"';<\/code> &#8212; \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043b\u0430\u0441\u0441 \u043a\u043d\u043e\u043f\u043a\u0438 \u0432\u043e \u0432\u0442\u043e\u0440\u043e\u043c \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0435.<\/p>\n<\/li>\n<li>\n<p><code>&lt;button ${btn_cls} onclick=... &lt;\/button&gt;<\/code> &#8212; \u043a\u043b\u0430\u0441\u0441 \u0432 \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u0430\u0445.<\/p>\n<\/li>\n<li>\n<p><code>EXPERIMENTS = {..., \"white_gold_btn\": {..., \"groups\": {'White': 50, 'Gold': 50}, ...}<\/code> &#8212; \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0432\u0442\u043e\u0440\u043e\u0433\u043e \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430.<\/p>\n<\/li>\n<\/ul>\n<p>\u041f\u0440\u0438 \u0437\u0430\u0445\u043e\u0434\u0435 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u043f\u043e\u043f\u0430\u0434\u0430\u0435\u0442 \u0432 \u043e\u0431\u0430 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430. \u0412 \u0437\u0430\u0445\u043e\u0434\u0430\u0445 <code>simulate_visits.py<\/code> \u0441\u043e\u043e\u0442\u043d\u043e\u0448\u0435\u043d\u0438\u0435 \u043c\u0435\u0436\u0434\u0443 \u0433\u0440\u0443\u043f\u043f\u0430\u043c\u0438 \u0431\u043b\u0438\u0437\u043a\u043e \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u043e\u043c\u0443. \u0412\u0435\u0440\u043e\u044f\u0442\u043d\u043e\u0441\u0442\u044c \u043a\u043b\u0438\u043a\u0430 \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0442 \u043f\u0435\u0440\u0432\u043e\u0433\u043e \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430: <code>CLICK_PROBS = {'Moon': 0.1, 'Mars': 0.2}<\/code>, \u0432\u0442\u043e\u0440\u043e\u0439 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442 \u043d\u0430 \u043d\u0435\u0435 \u043d\u0435 \u0432\u043b\u0438\u044f\u0435\u0442. \u041a\u043e\u043d\u0432\u0435\u0440\u0441\u0438\u0438 \u0432\u0442\u043e\u0440\u043e\u0433\u043e \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430 \u0432 \u043e\u0431\u0435\u0438\u0445 \u0433\u0440\u0443\u043f\u043f\u0430\u0445 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c <code>CLICK_PROBS['Moon']*share_Moon + CLICK_PROBS['Mars']*share_Mars<\/code>, \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0431\u043b\u0438\u0437\u043a\u0438 \u043a \u044d\u0442\u043e\u043c\u0443. \u041d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u044c \u0434\u0435\u043b\u0435\u043d\u0438\u044f \u0442\u0440\u0430\u0444\u0438\u043a\u0430 \u0432 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430\u0445 <code>P((exp1, group_i) and (exp2, group_j)) = P(exp1, group_i) * P(exp2, group_j)<\/code> \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u0435\u0442\u0441\u044f.<\/p>\n<pre><code class=\"bash\">&gt; python simulate_visits.py -n 1000  Moon\/Mars Exp Split: Mars: 502 visits (50.20%), Exact 50.00% Moon: 498 visits (49.80%), Exact 50.00%  White\/Gold Exp Split: Gold: 478 visits (47.80%), Exact 50.00% White: 522 visits (52.20%), Exact 50.00%  Moon\/Mars Exp events: Mars: 502 visits, 103 clicks, Conv=20.52 +- 3.60%, Exact: 20.00% Moon: 498 visits, 44 clicks, Conv=8.84 +- 2.54%, Exact: 10.00%  White\/Gold Exp events: Gold: 478 visits, 71 clicks, Conv=14.85 +- 3.25%, Exact: 15.00% White: 522 visits, 76 clicks, Conv=14.56 +- 3.09%, Exact: 15.00%  Split Independence moon_mars\/white_gold_btn: ('Mars', 'Gold'): 24.00%, independence 25.00% ('Mars', 'White'): 26.20%, independence 25.00% ('Moon', 'Gold'): 23.80%, independence 25.00% ('Moon', 'White'): 26.00%, independence 25.00%<\/code><\/pre>\n<p><a class=\"anchor\" name=\"%D0%90%D0%B4%D0%BC%D0%B8%D0%BD%D0%BA%D0%B0\" id=\"\u0410\u0434\u043c\u0438\u043d\u043a\u0430\"><\/a><\/p>\n<h2>7. \u0410\u0434\u043c\u0438\u043d\u043a\u0430<\/h2>\n<p>\u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0430\u0434\u043c\u0438\u043d\u043a\u0430 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u043e\u0432, \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u043e \u0432 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0445 \u0440\u0430\u0437\u0434\u0435\u043b\u0430\u0445. \u0412 \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0445 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0445 \u0434\u043b\u044f \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430\u043c\u0438 \u0447\u0430\u0441\u0442\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u0441\u0435\u0440\u0432\u0438\u0441.<\/p>\n<pre><code class=\"bash\">python 7_admin.py<\/code><\/pre>\n<p>\u042d\u043a\u0441\u043f: <a href=\"http:\/\/127.0.0.1:5000%EF%BF%BC\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000<\/a><br \/>\u0421\u043e\u0431\u044b\u0442\u0438\u044f: <a href=\"http:\/\/127.0.0.1:5000\/events%EF%BF%BC\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000\/events<\/a><br \/>\u042d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u044b: <a href=\"http:\/\/127.0.0.1:5000\/api\/experiments%EF%BF%BC\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000\/api\/experiments<\/a><br \/>\u0413\u0440\u0443\u043f\u043f\u044b: <a href=\"http:\/\/127.0.0.1:5000\/api\/expgroups\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000\/api\/expgroups<\/a><br \/>\u0410\u0434\u043c\u0438\u043d\u043a\u0430: <a href=\"http:\/\/127.0.0.1:5000\/experiments\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000\/experiments<\/a><\/p>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/299\/098\/02c\/29909802c99ef8b99953b971c2c5f8a6.png\" alt=\"\u0410\u0434\u043c\u0438\u043d\u043a\u0430 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u043e\u0432.\" title=\"\u0410\u0434\u043c\u0438\u043d\u043a\u0430 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u043e\u0432.\" width=\"1920\" height=\"1080\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/299\/098\/02c\/29909802c99ef8b99953b971c2c5f8a6.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/299\/098\/02c\/29909802c99ef8b99953b971c2c5f8a6.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>\u0410\u0434\u043c\u0438\u043d\u043a\u0430 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u043e\u0432.<\/figcaption><\/div>\n<\/figure>\n<pre><code class=\"python\"># ...  EXPERIMENTS = {     \"moon_mars\": {         \"title\": \"Moon\/Mars\",         \"groups\": {'Moon': 50, 'Mars': 50},         \"fallback\": \"Moon\",         \"state\": \"active\"     },     \"white_gold_btn\": {         \"title\": \"White\/Gold\",         \"groups\": {'White': 50, 'Gold': 50},         \"fallback\": \"White\",         \"state\": \"active\"     } }  EXPERIMENTS_TEMPLATE = \"\"\" &lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt;     &lt;title&gt;Experiments&lt;\/title&gt;     &lt;link rel=\"stylesheet\" href=\"{{ url_for('static', filename='admin.css') }}\"&gt; &lt;\/head&gt; &lt;body&gt;     &lt;h1&gt;Experiments&lt;\/h1&gt;     &lt;table&gt;         &lt;thead&gt;             &lt;tr&gt;                 &lt;th&gt;Experiment&lt;\/th&gt;                 &lt;th&gt;Key&lt;\/th&gt;                 &lt;th&gt;Group: Weight&lt;\/th&gt;                 &lt;th&gt;Fallback&lt;\/th&gt;                 &lt;th&gt;State&lt;\/th&gt;             &lt;\/tr&gt;         &lt;\/thead&gt;         &lt;tbody&gt;         {% for name, exp in experiments.items() %}             &lt;tr&gt;                 &lt;td&gt;{{ exp.title }}&lt;\/td&gt;                 &lt;td&gt;{{ name }}&lt;\/td&gt;                 &lt;td&gt;                     {% for g, w in exp.groups.items() %}                         {{ g }}: {{ w }} &lt;br&gt;                     {% endfor %}                 &lt;\/td&gt;                 &lt;td&gt;{{ exp.fallback }}&lt;\/td&gt;                 &lt;td&gt;{{ exp.state }}&lt;\/td&gt;             &lt;\/tr&gt;         {% endfor %}         &lt;\/tbody&gt;     &lt;\/table&gt; &lt;\/body&gt; &lt;\/html&gt; &lt;\/body&gt; &lt;\/html&gt; \"\"\"  @app.route('\/experiments', methods=['GET']) def experiments_page():     return render_template_string(EXPERIMENTS_TEMPLATE, experiments=EXPERIMENTS)  # ...<\/code><\/pre>\n<ul>\n<li>\n<p><code>EXPERIMENTS_TEMPLATE<\/code> \u2013 \u0448\u0430\u0431\u043b\u043e\u043d \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u043e\u0432.<\/p>\n<\/li>\n<li>\n<p><code>@app.route('\/experiments', methods=['GET'])<\/code> \u2013 \u043e\u0442\u0434\u0430\u0451\u0442 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u043e\u0432.<\/p>\n<\/li>\n<\/ul>\n<p>\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u043d\u0435 \u0432\u043b\u0438\u044f\u044e\u0442 \u043d\u0430 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u044b.<\/p>\n<p><a class=\"anchor\" name=\"%D0%92%D0%B5%D1%81%D0%B0\" id=\"\u0412\u0435\u0441\u0430\"><\/a><\/p>\n<h2>8. \u0412\u0435\u0441\u0430<\/h2>\n<p>\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u0432\u0435\u0441\u043e\u0432 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430 \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u0438\u0432\u0435\u0441\u0442\u0438 \u043a \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f\u043c \u0433\u0440\u0443\u043f\u043f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439. \u0425\u043e\u0442\u044f <code>hash(user_id || exp_name) % n_groups<\/code> \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u044b\u0439, \u0433\u0440\u0443\u043f\u043f\u0430 \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043e\u0442 \u0432\u0435\u0441\u043e\u0432. \u041d\u0443\u0436\u043d\u043e \u0437\u0430\u043f\u0438\u0441\u044b\u0432\u0430\u0442\u044c \u0432\u044b\u0434\u0430\u0432\u0430\u0435\u043c\u044b\u0435 \u0433\u0440\u0443\u043f\u043f\u044b \u0438 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0442\u044c \u0438\u0445 \u043f\u0440\u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u043c \u043e\u0431\u0440\u0430\u0449\u0435\u043d\u0438\u0438 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439. \u0412 \u043f\u0440\u0438\u043c\u0435\u0440\u0435 \u0433\u0440\u0443\u043f\u043f\u044b \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u044e\u0442\u0441\u044f \u043d\u0430 \u0431\u044d\u043a\u044d\u043d\u0434\u0435 \u0432 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 <code>ASSIGNEDGROUPS<\/code>.<\/p>\n<pre><code class=\"bash\">python 8_weights.py<\/code><\/pre>\n<p>\u042d\u043a\u0441\u043f: <a href=\"http:\/\/127.0.0.1:5000%EF%BF%BC\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000<\/a><br \/>\u0421\u043e\u0431\u044b\u0442\u0438\u044f: <a href=\"http:\/\/127.0.0.1:5000\/events%EF%BF%BC\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000\/events<\/a><br \/>\u042d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u044b: <a href=\"http:\/\/127.0.0.1:5000\/api\/experiments%EF%BF%BC\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000\/api\/experiments<\/a><br \/>\u0413\u0440\u0443\u043f\u043f\u044b: <a href=\"http:\/\/127.0.0.1:5000\/api\/expgroups\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000\/api\/expgroups<\/a><br \/>\u0410\u0434\u043c\u0438\u043d\u043a\u0430: <a href=\"http:\/\/127.0.0.1:5000\/experiments\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000\/experiments<\/a><\/p>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/cf6\/602\/04a\/cf660204a2a0df159185a492cefd9495.png\" alt=\"\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u0432\u0435\u0441\u043e\u0432 \u0433\u0440\u0443\u043f\u043f.\" title=\"\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u0432\u0435\u0441\u043e\u0432 \u0433\u0440\u0443\u043f\u043f.\" width=\"1920\" height=\"1080\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/cf6\/602\/04a\/cf660204a2a0df159185a492cefd9495.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/cf6\/602\/04a\/cf660204a2a0df159185a492cefd9495.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u0432\u0435\u0441\u043e\u0432 \u0433\u0440\u0443\u043f\u043f.<\/figcaption><\/div>\n<\/figure>\n<pre><code class=\"python\"># ...  EXPERIMENTS_TEMPLATE = \"\"\"     \/\/ ...     fetchExperiments().then(renderExperiments);     \/\/ ... \"\"\"  ASSIGNEDGROUPS = {}  def assign_group(device_id: str, experiment: str) -&gt; str:     if (device_id, experiment) in ASSIGNEDGROUPS:         gr, ts = ASSIGNEDGROUPS[(device_id, experiment)]         return gr     groups = EXPERIMENTS[experiment][\"groups\"]     total_parts = sum(groups.values())     key = f\"{device_id}:{experiment}\"     hash_bytes = hashlib.sha256(key.encode()).digest()     hash_int = int.from_bytes(hash_bytes, 'big')     hash_mod = hash_int % total_parts     c = 0     chosen = EXPERIMENTS[experiment][\"fallback\"]     for group_name, weight in sorted(groups.items()):         c += weight         if hash_mod &lt; c:             chosen = group_name             break     ASSIGNEDGROUPS[(device_id, experiment)] = (chosen, datetime.now().isoformat())     return chosen  @app.route('\/api\/experiments\/update', methods=['POST']) def update_experiment():     data = request.json     name = data.get(\"name\")     if not name or name not in EXPERIMENTS:         return jsonify({\"error\": \"Experiment not found\"}), 404     old_groups = set(EXPERIMENTS[name][\"groups\"].keys())     new_groups = set(data.get(\"groups\", {}).keys())     if old_groups != new_groups:         jsonify({\"error\": f\"Can't change {name} group weights\"}), 400     for g, w in data[\"groups\"].items():         try:             w_int = int(w)         except Exception as e:             return jsonify({\"error\": f\"Invalid weight for group '{g}': must be an integer\"}), 400         if w_int &lt;= 0:             return jsonify({\"error\": f\"Invalid weight for group '{g}': must be &gt; 0\"}), 400         data[\"groups\"][g] = w_int     for g in old_groups:         EXPERIMENTS[name][\"groups\"][g] = data[\"groups\"][g]     return jsonify({\"success\": True, \"experiment\": EXPERIMENTS[name]})  #...<\/code><\/pre>\n<ul>\n<li>\n<p><code>EXPERIMENTS_TEMPLATE<\/code> \u2013 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0432\u0435\u0441\u043e\u0432. <\/p>\n<\/li>\n<li>\n<p><code>ASSIGNEDGROUPS = {}<\/code> \u2013 \u0445\u0440\u0430\u043d\u0438\u0442 \u0432\u044b\u0434\u0430\u043d\u043d\u044b\u0435 \u0433\u0440\u0443\u043f\u043f\u044b.<\/p>\n<\/li>\n<li>\n<p><code>if (device_id, experiment) in ASSIGNEDGROUPS: ...<\/code> \u2013 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0440\u0430\u043d\u0435\u0435 \u0432\u044b\u0434\u0430\u043d\u043d\u0443\u044e \u0433\u0440\u0443\u043f\u043f\u0443, \u0435\u0441\u043b\u0438 \u043e\u043d\u0430 \u0435\u0441\u0442\u044c.<\/p>\n<\/li>\n<li>\n<p><code>ASSIGNEDGROUPS[(device_id, experiment)] = (chosen, <\/code><a href=\"http:\/\/datetime.now\" rel=\"noopener noreferrer nofollow\"><code>datetime.now<\/code><\/a><code>().isoformat())<\/code> \u2013 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442 \u0433\u0440\u0443\u043f\u043f\u0443 \u0438 \u0432\u0440\u0435\u043c\u044f \u043f\u0440\u0438\u0441\u0432\u043e\u0435\u043d\u0438\u044f.<\/p>\n<\/li>\n<li>\n<p><code>@app.route('\/api\/experiments\/update', methods=['POST'])<\/code> \u2013 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u0442 \u0432\u0435\u0441\u0430 \u0433\u0440\u0443\u043f\u043f.<\/p>\n<p>\u0414\u0435\u043b\u0435\u043d\u0438\u0435 \u0442\u0440\u0430\u0444\u0438\u043a\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0430\u0434\u043c\u0438\u043d\u043a\u0435.<\/p>\n<\/li>\n<\/ul>\n<pre><code class=\"bash\">&gt; python simulate_visits.py -n 1000  Moon\/Mars Exp Split: Mars: 238 visits (23.80%), Exact 25.00% Moon: 762 visits (76.20%), Exact 75.00%  White\/Gold Exp Split: Gold: 505 visits (50.50%), Exact 50.00% White: 495 visits (49.50%), Exact 50.00%  Moon\/Mars Exp events: Mars: 238 visits, 52 clicks, Conv=21.85 +- 5.36%, Exact: 20.00% Moon: 762 visits, 81 clicks, Conv=10.63 +- 2.23%, Exact: 10.00%  White\/Gold Exp events: Gold: 505 visits, 68 clicks, Conv=13.47 +- 3.04%, Exact: 12.50% White: 495 visits, 65 clicks, Conv=13.13 +- 3.04%, Exact: 12.50%  Split Independence moon_mars\/white_gold_btn: ('Mars', 'Gold'): 11.60%, independence 12.50% ('Mars', 'White'): 12.20%, independence 12.50% ('Moon', 'Gold'): 38.90%, independence 37.50% ('Moon', 'White'): 37.30%, independence 37.50%<\/code><\/pre>\n<p><a class=\"anchor\" name=\"%D0%A0%D0%B0%D1%81%D0%BA%D0%B0%D1%82%D0%BA%D0%B0\" id=\"\u0420\u0430\u0441\u043a\u0430\u0442\u043a\u0430\"><\/a><\/p>\n<h2>9. \u0420\u0430\u0441\u043a\u0430\u0442\u043a\u0430<\/h2>\n<p>\u0423 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u043e\u0432 \u0442\u0440\u0438 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f: &#171;\u043d\u0435\u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0439&#187;, &#171;\u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0439&#187;, &#171;\u0440\u0430\u0441\u043a\u0430\u0442\u0430\u043d&#187;. \u0412 \u043d\u0435\u0430\u043a\u0442\u0438\u0432\u043d\u043e\u043c \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438 \u043f\u043e\u043b\u0443\u0447\u0430\u044e\u0442 \u0434\u0435\u0444\u043e\u043b\u0442\u043d\u044b\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442, \u0433\u0440\u0443\u043f\u043f\u044b \u043d\u0435 \u0437\u0430\u043f\u0438\u0441\u044b\u0432\u0430\u044e\u0442\u0441\u044f. \u0412 \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u043c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438 \u0440\u0430\u0441\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u044e\u0442\u0441\u044f \u043f\u043e \u0433\u0440\u0443\u043f\u043f\u0430\u043c, \u0432\u044b\u0434\u0430\u043d\u043d\u044b\u0435 \u0433\u0440\u0443\u043f\u043f\u044b \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u044e\u0442\u0441\u044f \u0432 <code>ASSIGNEDGROUPS<\/code>. \u041f\u043e\u0441\u043b\u0435 \u0440\u0430\u0441\u043a\u0430\u0442\u043a\u0438 \u0432\u0441\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438 \u0432\u0438\u0434\u044f\u0442 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u0443\u044e \u0440\u0430\u0441\u043a\u0430\u0442\u0430\u043d\u043d\u0443\u044e \u0433\u0440\u0443\u043f\u043f\u0443, \u0432\u044b\u0434\u0430\u043d\u043d\u044b\u0435 \u0440\u0430\u043d\u0435\u0435 \u0433\u0440\u0443\u043f\u043f\u044b \u0438\u0433\u043d\u043e\u0440\u0438\u0440\u0443\u044e\u0442\u0441\u044f.<\/p>\n<p>\u041f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0435 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430 (\u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 &#171;\u043d\u0435\u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0439&#187;-&#171;\u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0439&#187;) \u0437\u0430\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0432\u0440\u0435\u043c\u044f \u0437\u0430\u043f\u0443\u0441\u043a\u0430. \u0414\u043b\u044f \u0440\u0430\u0441\u043a\u0430\u0442\u043a\u0438 \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0433\u043e \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u0431\u0440\u0430\u0442\u044c \u0433\u0440\u0443\u043f\u043f\u0443 \u0440\u0430\u0441\u043a\u0430\u0442\u043a\u0438. \u041f\u0440\u0438 \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0432 &#171;\u0440\u0430\u0441\u043a\u0430\u0442\u0430\u043d&#187; \u0444\u0438\u043a\u0441\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u0432\u0440\u0435\u043c\u044f \u043e\u043a\u043e\u043d\u0447\u0430\u043d\u0438\u044f \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430. \u041e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430 (\u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 &#171;\u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0439&#187;-&#171;\u043d\u0435\u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0439&#187;) \u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u0438\u0441\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u043e\u0448\u0438\u0431\u043e\u043a: \u0432\u0441\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438 \u043f\u0435\u0440\u0435\u0432\u043e\u0434\u044f\u0442\u0441\u044f \u043d\u0430 \u0434\u0435\u0444\u043e\u043b\u0442\u043d\u044b\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442. \u0412\u044b\u0434\u0430\u043d\u043d\u044b\u0435 \u0440\u0430\u043d\u0435\u0435 \u0433\u0440\u0443\u043f\u043f\u044b \u043e\u0441\u0442\u0430\u044e\u0442\u0441\u044f \u0438 \u043f\u043e\u0441\u043b\u0435 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u043a\u0442\u0438\u0432\u0430\u0446\u0438\u0438 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438 \u043f\u043e\u043b\u0443\u0447\u0430\u0442 \u0441\u0442\u0430\u0440\u044b\u0435 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u044b. \u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439, \u0443 \u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u043c\u0435\u043d\u044f\u043b\u0438\u0441\u044c \u0433\u0440\u0443\u043f\u043f\u044b, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0438\u0441\u043a\u043b\u044e\u0447\u0430\u0442\u044c \u0438\u0437 \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0438. \u0413\u0440\u0443\u043f\u043f\u044b \u043f\u043e\u043c\u0435\u043d\u044f\u044e\u0442\u0441\u044f \u0443 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u0441 \u043d\u0435\u0434\u0435\u0444\u043e\u043b\u0442\u043d\u044b\u043c \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u043e\u043c, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0438\u0445 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043d\u0430\u0440\u0443\u0448\u0438\u0442 \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u043e\u0435 \u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u043c\u0435\u0436\u0434\u0443 \u0433\u0440\u0443\u043f\u043f\u0430\u043c\u0438 \u0438 \u043f\u0440\u0438\u0432\u0435\u0434\u0435\u0442 \u043a \u0434\u0438\u0437\u0431\u0430\u043b\u0430\u043d\u0441\u0443 \u0441\u0435\u0433\u043c\u0435\u043d\u0442\u043e\u0432. \u041d\u0430\u0434\u0451\u0436\u043d\u0435\u0435 \u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e \u0438\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0432\u0441\u0435 \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438 \u0443\u0447\u0438\u0442\u044b\u0432\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439, \u0432\u043f\u0435\u0440\u0432\u044b\u0435 \u043f\u043e\u043f\u0430\u0432\u0448\u0438\u0445 \u0432 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442 \u043f\u043e\u0441\u043b\u0435 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u043a\u0442\u0438\u0432\u0430\u0446\u0438\u0438. \u041f\u0435\u0440\u0435\u0445\u043e\u0434 \u0438\u0437 \u0440\u0430\u0441\u043a\u0430\u0442\u0430\u043d\u043d\u043e\u0433\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0432 \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0435 \u0442\u0440\u0430\u043a\u0442\u0443\u0435\u0442\u0441\u044f \u043a\u0430\u043a \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a: \u043f\u043e\u043f\u0430\u0432\u0448\u0438\u0445 \u0440\u0430\u043d\u0435\u0435 \u0432 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u043d\u0443\u0436\u043d\u043e \u0438\u0441\u043a\u043b\u044e\u0447\u0430\u0442\u044c \u0438\u0437 \u0430\u043d\u0430\u043b\u0438\u0437\u0430 \u0430\u043d\u0430\u043b\u043e\u0433\u0438\u0447\u043d\u043e \u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0435 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430. \u041f\u0440\u044f\u043c\u043e\u0439 \u043f\u0435\u0440\u0435\u0445\u043e\u0434 \u043c\u0435\u0436\u0434\u0443 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f\u043c\u0438 &#171;\u043d\u0435\u0430\u043a\u0442\u0438\u0432\u0435\u043d&#187; \u0438 &#171;\u0440\u0430\u0441\u043a\u0430\u0442\u0430\u043d&#187; \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d.<\/p>\n<pre><code class=\"bash\">python 9_rollout.py<\/code><\/pre>\n<p>\u042d\u043a\u0441\u043f: <a href=\"http:\/\/127.0.0.1:5000%EF%BF%BC\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000<\/a><br \/>\u0421\u043e\u0431\u044b\u0442\u0438\u044f: <a href=\"http:\/\/127.0.0.1:5000\/events%EF%BF%BC\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000\/events<\/a><br \/>\u042d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u044b: <a href=\"http:\/\/127.0.0.1:5000\/api\/experiments%EF%BF%BC\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000\/api\/experiments<\/a><br \/>\u0413\u0440\u0443\u043f\u043f\u044b: <a href=\"http:\/\/127.0.0.1:5000\/api\/expgroups\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000\/api\/expgroups<\/a><br \/>\u0410\u0434\u043c\u0438\u043d\u043a\u0430: <a href=\"http:\/\/127.0.0.1:5000\/experiments\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000\/experiments<\/a><\/p>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/8aa\/509\/03c\/8aa50903ca2b366f1670a6781eb52b9e.png\" alt=\"\u0420\u0430\u0441\u043a\u0430\u0442\u043a\u0430.\" title=\"\u0420\u0430\u0441\u043a\u0430\u0442\u043a\u0430.\" width=\"1920\" height=\"1080\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/8aa\/509\/03c\/8aa50903ca2b366f1670a6781eb52b9e.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/8aa\/509\/03c\/8aa50903ca2b366f1670a6781eb52b9e.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>\u0420\u0430\u0441\u043a\u0430\u0442\u043a\u0430.<\/figcaption><\/div>\n<\/figure>\n<pre><code class=\"python\"># ...  EXPERIMENTS = {     \"moon_mars\": {         \"title\": \"Moon\/Mars\",         \"groups\": {'Moon': 50, 'Mars': 50},         \"fallback\": \"Moon\",         \"state\": \"active\",         \"rollout_group\": None,         \"start\": datetime.now().isoformat(),         \"end\": None     },     \"white_gold_btn\": {         \"title\": \"White\/Gold\",         \"groups\": {'White': 50, 'Gold': 50},         \"fallback\": \"White\",         \"state\": \"inactive\",         \"rollout_group\": None,         \"start\": None,         \"end\": None     } }  def assign_group(device_id: str, experiment: str) -&gt; str:     if EXPERIMENTS[experiment][\"state\"] == \"rollout\":         return EXPERIMENTS[experiment][\"rollout_group\"]     elif EXPERIMENTS[experiment][\"state\"] == \"inactive\":         return EXPERIMENTS[experiment][\"fallback\"]     if (device_id, experiment) in ASSIGNEDGROUPS:         gr, ts = ASSIGNEDGROUPS[(device_id, experiment)]         return gr     groups = EXPERIMENTS[experiment][\"groups\"]     total_parts = sum(groups.values())     key = f\"{device_id}:{experiment}\"     hash_bytes = hashlib.sha256(key.encode()).digest()     hash_int = int.from_bytes(hash_bytes, 'big')     hash_mod = hash_int % total_parts     c = 0     chosen = EXPERIMENTS[experiment][\"fallback\"]     for group_name, weight in sorted(groups.items()):         c += weight         if hash_mod &lt; c:             chosen = group_name             break     ASSIGNEDGROUPS[(device_id, experiment)] = (chosen, datetime.now().isoformat())     return chosen  @app.route('\/api\/experiments\/update', methods=['POST']) def update_experiment():     data = request.json     name = data.get(\"name\")     if not name or name not in EXPERIMENTS:         return jsonify({\"error\": \"Experiment not found\"}), 404     current_state = EXPERIMENTS[name][\"state\"]     new_state = data.get(\"state\", current_state)     allowed_transitions = [(\"inactive\", \"inactive\"),                            (\"inactive\", \"active\"),                            (\"active\", \"inactive\"),                            (\"active\", \"active\"),                            (\"active\", \"rollout\"),                            (\"rollout\", \"rollout\"),                            (\"rollout\", \"active\")]     if not (current_state, new_state) in allowed_transitions:         return jsonify({\"error\": f\"Can't change state from {current_state} to {new_state}\"}), 400     rollout_group = data.get(\"rollout_group\")     if new_state == \"rollout\" and rollout_group not in EXPERIMENTS[name][\"groups\"]:         return jsonify({\"error\": \"Invalid rollout group\"}), 400     EXPERIMENTS[name][\"state\"] = new_state     if current_state == \"inactive\" and new_state == \"active\":         EXPERIMENTS[name][\"start\"] = datetime.now().isoformat()         EXPERIMENTS[name][\"end\"] = None     elif current_state == \"active\" and new_state == \"inactive\":         EXPERIMENTS[name][\"end\"] = datetime.now().isoformat()     elif current_state == \"active\" and new_state == \"rollout\":         EXPERIMENTS[name][\"rollout_group\"] = rollout_group         EXPERIMENTS[name][\"end\"] = datetime.now().isoformat()     elif current_state == \"rollout\" and new_state == \"rollout\":         EXPERIMENTS[name][\"rollout_group\"] = rollout_group     elif current_state == \"rollout\" and new_state == \"active\":         EXPERIMENTS[name][\"rollout_group\"] = None         EXPERIMENTS[name][\"start\"] = datetime.now().isoformat()         EXPERIMENTS[name][\"end\"] = None     if new_state != \"rollout\":         old_groups = set(EXPERIMENTS[name][\"groups\"].keys())         new_groups = set(data.get(\"groups\", {}).keys())         if old_groups != new_groups:             jsonify({\"error\": f\"Can't change {name} group weights\"}), 400         for g, w in data[\"groups\"].items():             try:                 w_int = int(w)             except Exception as e:                 return jsonify({\"error\": f\"Invalid weight for group '{g}': must be an integer\"}), 400             if w_int &lt;= 0:                 return jsonify({\"error\": f\"Invalid weight for group '{g}': must be &gt; 0\"}), 400             data[\"groups\"][g] = w_int         for g in old_groups:             EXPERIMENTS[name][\"groups\"][g] = data[\"groups\"][g]     return jsonify({\"success\": True, \"experiment\": EXPERIMENTS[name]})  # ...<\/code><\/pre>\n<ul>\n<li>\n<p><code>EXPERIMENTS = {... {...\"state\": \"active\",...}...}<\/code> &#8212; \u0445\u0440\u0430\u043d\u0438\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430, \u0440\u0430\u0441\u043a\u0430\u0442\u0430\u043d\u043d\u0443\u044e \u0433\u0440\u0443\u043f\u043f\u0443, \u0432\u0440\u0435\u043c\u044f \u043d\u0430\u0447\u0430\u043b\u0430 \u0438 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430. <\/p>\n<\/li>\n<li>\n<p><code>def assign_group(...)<\/code> &#8212; \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0434\u0435\u0444\u043e\u043b\u0442\u043d\u0443\u044e \u0433\u0440\u0443\u043f\u043f\u0443 \u0434\u043b\u044f \u043d\u0435\u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0445 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u043e\u0432, \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u0443\u044e \u0433\u0440\u0443\u043f\u043f\u0443 \u0440\u0430\u0441\u043a\u0430\u0442\u043a\u0438 \u0434\u043b\u044f \u0440\u0430\u0441\u043a\u0430\u0442\u0430\u043d\u043d\u044b\u0445 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u0435\u043d\u0442\u043e\u0432. \u0414\u043b\u044f \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0445 \u043e\u0442\u0434\u0430\u0435\u0442 \u0440\u0430\u043d\u0435\u0435 \u0432\u044b\u0434\u0430\u043d\u043d\u0443\u044e \u0433\u0440\u0443\u043f\u043f\u0443 \u0438\u043b\u0438 \u043d\u0430\u0437\u043d\u0430\u0447\u0430\u0435\u0442 \u043d\u043e\u0432\u0443\u044e.<\/p>\n<\/li>\n<li>\n<p><code>def update_experiment()<\/code> &#8212; \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430 \u0438 \u0432\u0435\u0441\u0430 \u0433\u0440\u0443\u043f\u043f \u0438\u0437 \u0430\u0434\u043c\u0438\u043d\u043a\u0438.<\/p>\n<\/li>\n<\/ul>\n<p>\u042d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442 &#171;Moon\/Mars&#187; \u0430\u043a\u0442\u0438\u0432\u0435\u043d \u0441 \u0434\u0435\u043b\u0435\u043d\u0438\u0435\u043c \u043f\u043e \u0433\u0440\u0443\u043f\u043f\u0430\u043c 50\/50. \u042d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442 &#171;White\/Gold&#187; \u0440\u0430\u0441\u043a\u0430\u0442\u0430\u043d \u0438 \u043e\u0442\u0434\u0430\u0451\u0442 \u0432\u0441\u0435\u043c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f\u043c \u0442\u043e\u043b\u044c\u043a\u043e \u0440\u0430\u0441\u043a\u0430\u0442\u0430\u043d\u043d\u0443\u044e \u0433\u0440\u0443\u043f\u043f\u0443.<\/p>\n<pre><code class=\"bash\">&gt; python simulate_visits.py -n 1000  Moon\/Mars Exp Split: Mars: 472 visits (47.20%), Exact 50.00% Moon: 528 visits (52.80%), Exact 50.00%  White\/Gold Exp Split: White: 1000 visits (100.00%), Exact 100.00%  Moon\/Mars Exp events: Mars: 472 visits, 109 clicks, Conv=23.09 +- 3.88%, Exact: 20.00% Moon: 528 visits, 54 clicks, Conv=10.23 +- 2.64%, Exact: 10.00%  White\/Gold Exp events: White: 1000 visits, 163 clicks, Conv=16.30 +- 2.34%, Exact: 15.00%  Split Independence moon_mars\/white_gold_btn: ('Mars', 'White'): 47.20%, independence 50.00% ('Moon', 'White'): 52.80%, independence 50.00%<\/code><\/pre>\n<p><a class=\"anchor\" name=\"%D0%97%D0%B0%D0%BA%D0%BB%D1%8E%D1%87%D0%B5%D0%BD%D0%B8%D0%B5\" id=\"\u0417\u0430\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435\"><\/a><\/p>\n<h2>\u0417\u0430\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435<\/h2>\n<p>\u0414\u043b\u044f A\/\u0411\u2011\u0442\u0435\u0441\u0442\u043e\u0432 \u0432 \u0432\u0435\u0431\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u044b \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0433\u0440\u0443\u043f\u043f, \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u043b\u043e\u0433\u0438\u043a\u0438 \u043d\u0430 \u0431\u044d\u043a\u044d\u043d\u0434\u0435 \u0438 \u0444\u0440\u043e\u043d\u0442\u044d\u043d\u0434\u0435, \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0430 \u0430\u043d\u0430\u043b\u0438\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0438 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430\u043c\u0438. \u041f\u0440\u0438\u043c\u0435\u0440\u044b \u0434\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0438\u0440\u0443\u044e\u0442 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044e \u0410\/\u0411-\u0442\u0435\u0441\u0442\u043e\u0432 \u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u043e\u0432.<\/p>\n<p>\u0424\u043e\u0442\u043e:<br \/><code>static\/moon.jpg<\/code>: <a href=\"https:\/\/commons.wikimedia.org\/wiki\/File:Full_disc_of_the_moon_was_photographed_by_the_Apollo_17_crewmen.jpg\" rel=\"noopener noreferrer nofollow\">NASA, Public domain, via Wikimedia Commons<\/a><br \/><code>static\/mars.jpg<\/code>: <a href=\"https:\/\/commons.wikimedia.org\/wiki\/File:Mars_Daily_Global_Image_from_April_1999.jpg\" rel=\"noopener noreferrer nofollow\">NASA\/JPL, Public domain, via Wikimedia Commons<\/a><\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/940118\/\"> https:\/\/habr.com\/ru\/articles\/940118\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p><em>\u0414\u043b\u044f \u0410\/\u0411-\u0442\u0435\u0441\u0442\u043e\u0432 \u0432 \u0432\u0435\u0431\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u044b \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u0439 \u0432\u044b\u0431\u043e\u0440 \u0433\u0440\u0443\u043f\u043f, \u0445\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435, \u043b\u043e\u0433\u0438\u043a\u0430 \u043d\u0430 \u0431\u044d\u043a\u044d\u043d\u0434\u0435 \u0438 \u0444\u0440\u043e\u043d\u0442\u044d\u043d\u0434\u0435, \u043b\u043e\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u0439, \u043e\u0434\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0435 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u044b \u0438 \u0430\u0434\u043c\u0438\u043d\u043a\u0430.<\/em><\/p>\n<p><a href=\"#%D0%A1%D0%BB%D1%83%D1%87%D0%B0%D0%B9%D0%BD%D1%8B%D0%B5-%D0%B3%D1%80%D1%83%D0%BF%D0%BF%D1%8B\" rel=\"noopener noreferrer nofollow\"><em>1. \u0421\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u0435 \u0433\u0440\u0443\u043f\u043f\u044b<\/em><\/a><br \/><a href=\"#%D0%A5%D1%8D%D1%88%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5\" rel=\"noopener noreferrer nofollow\"><em>2. \u0425\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435<\/em><\/a><br \/><a href=\"#%D0%A4%D1%80%D0%BE%D0%BD%D1%82%D1%8D%D0%BD%D0%B4\" rel=\"noopener noreferrer nofollow\"><em>3. \u0424\u0440\u043e\u043d\u0442\u044d\u043d\u0434<\/em><\/a><br \/><a href=\"#%D0%A1%D0%BE%D0%B1%D1%8B%D1%82%D0%B8%D1%8F\" rel=\"noopener noreferrer nofollow\"><em>4. \u0421\u043e\u0431\u044b\u0442\u0438\u044f<\/em><\/a><br \/><a href=\"#%D0%9A%D0%BE%D0%BD%D1%84%D0%B8%D0%B3\" rel=\"noopener noreferrer nofollow\"><em>5. \u041a\u043e\u043d\u0444\u0438\u0433<\/em><\/a><br \/><a href=\"#%D0%94%D0%B2%D0%B0-%D1%8D%D0%BA%D1%81%D0%BF%D0%B5%D1%80%D0%B8%D0%BC%D0%B5%D0%BD%D1%82%D0%B0\" rel=\"noopener noreferrer nofollow\"><em>6. \u0414\u0432\u0430 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430<\/em><\/a><br \/><a href=\"#%D0%90%D0%B4%D0%BC%D0%B8%D0%BD%D0%BA%D0%B0\" rel=\"noopener noreferrer nofollow\"><em>7. \u0410\u0434\u043c\u0438\u043d\u043a\u0430<\/em><\/a><br \/><a href=\"#%D0%92%D0%B5%D1%81%D0%B0\" rel=\"noopener noreferrer nofollow\"><em>8. \u0412\u0435\u0441\u0430<\/em><\/a><br \/><a href=\"#%D0%A0%D0%B0%D1%81%D0%BA%D0%B0%D1%82%D0%BA%D0%B0\" rel=\"noopener noreferrer nofollow\"><em>9. \u0420\u0430\u0441\u043a\u0430\u0442\u043a\u0430<\/em><\/a><br \/><a href=\"#%D0%97%D0%B0%D0%BA%D0%BB%D1%8E%D1%87%D0%B5%D0%BD%D0%B8%D0%B5\" rel=\"noopener noreferrer nofollow\"><em>\u0417\u0430\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435<\/em><\/a><\/p>\n<p>\u0420\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0439: <a href=\"https:\/\/github.com\/andrewbrdk\/AB-Testing-from-Scratch\" rel=\"noopener noreferrer nofollow\">https:\/\/github.com\/andrewbrdk\/AB-Testing-from-Scratch<\/a> .<\/p>\n<p>\u0412\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0435 \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u0435 \u043f\u0440\u0438\u043c\u0435\u0440\u043e\u0432:<\/p>\n<pre><code class=\"bash\">git clone https:\/\/github.com\/andrewbrdk\/Web-AB-Testing-Demo cd Web-AB-Testing-Demo python -m venv pyvenv source .\/pyvenv\/bin\/activate pip install flask aiohttp playwright playwright install chromium<\/code><\/pre>\n<p>A\/\u0411-\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0432\u0435\u0431-\u0441\u0435\u0440\u0432\u0438\u0441\u043e\u0432 \u043e\u0446\u0435\u043d\u0438\u0432\u0430\u0435\u0442 \u0432\u043b\u0438\u044f\u043d\u0438\u0435 \u043d\u043e\u0432\u044b\u0445 \u0444\u0443\u043d\u043a\u0446\u0438\u0439 \u043d\u0430 \u043a\u043b\u044e\u0447\u0435\u0432\u044b\u0435 \u043c\u0435\u0442\u0440\u0438\u043a\u0438. \u041e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u044c\u043d\u0430\u044f \u0438 \u0438\u0437\u043c\u0435\u043d\u0451\u043d\u043d\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u044e\u0442\u0441\u044f \u043f\u0430\u0440\u0430\u043b\u043b\u0435\u043b\u044c\u043d\u043e, \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438 \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u043e \u0434\u0435\u043b\u044f\u0442\u0441\u044f \u043c\u0435\u0436\u0434\u0443 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0430\u043c\u0438. \u041e\u0434\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0439 \u0437\u0430\u043f\u0443\u0441\u043a \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432\u0430\u0435\u0442 \u0440\u0430\u0432\u043d\u043e\u0435 \u0432\u043b\u0438\u044f\u043d\u0438\u0435 \u0432\u043d\u0435\u0448\u043d\u0438\u0445 \u0444\u0430\u043a\u0442\u043e\u0440\u043e\u0432, \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u043e\u0435 \u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u0441\u043d\u0438\u0436\u0430\u0435\u0442 \u0434\u0438\u0437\u0431\u0430\u043b\u0430\u043d\u0441 \u0433\u0440\u0443\u043f\u043f. \u0412 \u0438\u0442\u043e\u0433\u0435 \u0440\u0430\u0437\u043b\u0438\u0447\u0438\u044f \u043c\u0435\u0442\u0440\u0438\u043a \u043c\u0435\u0436\u0434\u0443 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0430\u043c\u0438 \u043e\u0431\u044a\u044f\u0441\u043d\u044f\u044e\u0442 \u043d\u043e\u0432\u043e\u0439 \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c\u044e.<\/p>\n<p>\u0414\u043b\u044f \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e\u0439 \u043e\u0446\u0435\u043d\u043a\u0438 \u044d\u0444\u0444\u0435\u043a\u0442\u0430 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438 \u0434\u043e\u043b\u0436\u043d\u044b \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u043e\u0432\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u0441 \u043e\u0434\u043d\u043e\u0439 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0435\u0439, \u043f\u043e\u043f\u0430\u0432\u0448\u0438\u0445 \u0432 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0433\u0440\u0443\u043f\u043f \u043d\u0443\u0436\u043d\u043e \u0438\u0441\u043a\u043b\u044e\u0447\u0430\u0442\u044c \u043f\u0440\u0438 \u043f\u043e\u0434\u0432\u0435\u0434\u0435\u043d\u0438\u0438 \u0438\u0442\u043e\u0433\u043e\u0432. \u041d\u0430 \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0435 \u0433\u0440\u0443\u043f\u043f\u0443 \u043f\u0440\u0438\u0432\u044f\u0437\u044b\u0432\u0430\u044e\u0442 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 \u0438\u043b\u0438 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0443, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043f\u0440\u0438 \u0441\u043c\u0435\u043d\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043c\u043e\u0436\u0435\u0442 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c\u0441\u044f \u0434\u0440\u0443\u0433\u043e\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442.<\/p>\n<p><a class=\"anchor\" name=\"%D0%A1%D0%BB%D1%83%D1%87%D0%B0%D0%B9%D0%BD%D1%8B%D0%B5-%D0%B3%D1%80%D1%83%D0%BF%D0%BF%D1%8B\" id=\"\u0421\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u0435-\u0433\u0440\u0443\u043f\u043f\u044b\">-\u0433\u0440\u0443\u043f\u043f\u044b&#187;><\/a><\/p>\n<h2>1. \u0421\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u0435 \u0433\u0440\u0443\u043f\u043f\u044b<\/h2>\n<p>\u042d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442\u0441\u044f \u043d\u0430 \u0431\u044d\u043a\u044d\u043d\u0434\u0435 \u0432\u044b\u0437\u043e\u0432\u043e\u043c <code>random.choice<\/code>. \u0413\u0440\u0443\u043f\u043f\u0430 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442\u0441\u044f \u0432 \u043a\u0443\u043a\u0430\u0445 \u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442\u0441\u044f \u043f\u0440\u0438 \u043a\u0430\u0436\u0434\u043e\u043c \u0437\u0430\u0445\u043e\u0434\u0435 \u043d\u0430 \u0441\u0430\u0439\u0442 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e\u0441\u0442\u0438 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0430.<\/p>\n<pre><code class=\"bash\">python 1_rnd.py<\/code><\/pre>\n<p>\u042d\u043a\u0441\u043f: <a href=\"http:\/\/127.0.0.1:5000\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000<\/a><\/p>\n<figure class=\"full-width\">\n<div><figcaption>\u0412\u0430\u0440\u0438\u0430\u043d\u0442\u044b \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430.<\/figcaption><\/div>\n<\/figure>\n<pre><code class=\"python\">from flask import Flask, render_template_string, request, make_response import random  app = Flask(__name__)  TEMPLATE = ''' &lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt;     &lt;title&gt;A\/B Test&lt;\/title&gt;     &lt;link rel=\"stylesheet\" href=\"{{ url_for('static', filename='banners.css') }}\"&gt; &lt;\/head&gt; &lt;body&gt;     {% if variant == 'Moon' %}         &lt;div class=\"banner\" style=\"background-image: url('{{ url_for('static', filename='.\/moon.jpg') }}');\"&gt;             &lt;h1&gt;Walk on the Moon&lt;\/h1&gt;             &lt;div class=\"vspacer\"&gt;&lt;\/div&gt;             &lt;p&gt;Be one of the first tourists to set foot on the lunar surface. Your journey to another world starts here.&lt;\/p&gt;             &lt;button onclick=\"console.log('Click Moon')\"&gt;Reserve Your Spot&lt;\/button&gt;         &lt;\/div&gt;     {% else %}         &lt;div class=\"banner\" style=\"background-image: url('{{ url_for('static', filename='.\/mars.jpg') }}');\"&gt;             &lt;h1&gt;Journey to Mars&lt;\/h1&gt;             &lt;div class=\"vspacer\"&gt;&lt;\/div&gt;             &lt;p&gt;Be among the first humans to set foot on the Red Planet. Experience the adventure of a lifetime.&lt;\/p&gt;             &lt;button onclick=\"console.log('Click Mars')\"&gt;Reserve Your Spot&lt;\/button&gt;         &lt;\/div&gt;     {% endif %} &lt;\/body&gt; &lt;\/html&gt; '''  @app.route('\/') def index():     variant = request.cookies.get('variant')     if variant not in ['Moon', 'Mars']:         variant = random.choice(['Moon', 'Mars'])     response = make_response(render_template_string(TEMPLATE, variant=variant))     response.set_cookie('variant', variant, max_age=60*60*24*30)     return response  if __name__ == '__main__':     app.run(debug=True)<\/code><\/pre>\n<ul>\n<li>\n<p><code>{% if variant == 'Moon' %} ... {% endif %}<\/code> &#8212; \u0431\u044d\u043a\u0435\u043d\u0434 \u043e\u0442\u0434\u0430\u0451\u0442 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u0433\u0440\u0443\u043f\u043f\u0435 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b.<\/p>\n<\/li>\n<li>\n<p><code>variant = request.cookies.get('variant')<\/code> &#8212; \u0441\u0447\u0438\u0442\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0442\u0435\u043a\u0443\u0449\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430 \u0432 \u043a\u0443\u043a\u0430\u0445.<\/p>\n<\/li>\n<li>\n<p><code>variant = random.choice(['Moon', 'Mars'])<\/code> &#8212; \u0435\u0441\u043b\u0438 \u0433\u0440\u0443\u043f\u043f\u044b \u043d\u0435\u0442, \u0433\u0440\u0443\u043f\u043f\u0430 \u0432\u044b\u0431\u0438\u0440\u0430\u0435\u0442\u0441\u044f \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u043e.<\/p>\n<\/li>\n<li>\n<p><code>response.set_cookie('variant', variant, max_age=60*60*24*30)<\/code> &#8212; \u0433\u0440\u0443\u043f\u043f\u0430 \u0437\u0430\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0432 \u043a\u0443\u043a\u0438.<\/p>\n<\/li>\n<\/ul>\n<p>\u0414\u043b\u044f \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0430 \u0434\u0440\u0443\u0433\u043e\u0433\u043e \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0430 \u043d\u0443\u0436\u043d\u043e \u043b\u0438\u0431\u043e \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0432 \u043d\u043e\u0432\u043e\u043c \u043e\u043a\u043d\u0435 \u0438\u043d\u043a\u043e\u0433\u043d\u0438\u0442\u043e, \u043b\u0438\u0431\u043e \u043e\u0447\u0438\u0441\u0442\u0438\u0442\u044c \u043a\u0443\u043a\u0438 \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443.<\/p>\n<figure class=\"full-width\">\n<div><figcaption>\u042d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442\u0441\u044f \u0432 \u043a\u0443\u043a\u0430\u0445 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e\u0441\u0442\u0438 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0430 \u043f\u0440\u0438 \u0437\u0430\u0445\u043e\u0434\u0435 \u043d\u0430 \u0441\u0430\u0439\u0442. \u0414\u043b\u044f \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0430 \u0434\u0440\u0443\u0433\u043e\u0433\u043e \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0430 \u043d\u0443\u0436\u043d\u043e \u043b\u0438\u0431\u043e \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0432 \u043d\u043e\u0432\u043e\u043c \u043e\u043a\u043d\u0435 \u0438\u043d\u043a\u043e\u0433\u043d\u0438\u0442\u043e, \u043b\u0438\u0431\u043e \u043e\u0447\u0438\u0441\u0442\u0438\u0442\u044c \u043a\u0443\u043a\u0438 \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443.<\/figcaption><\/div>\n<\/figure>\n<p>\u0421\u043a\u0440\u0438\u043f\u0442 <a href=\"https:\/\/github.com\/andrewbrdk\/AB-Testing-from-Scratch\/blob\/main\/simulate_visits.py\" rel=\"noopener noreferrer nofollow\"><code>simulate_visits.py<\/code><\/a> \u0438\u043c\u0438\u0442\u0438\u0440\u0443\u0435\u0442 \u0437\u0430\u0445\u043e\u0434\u044b \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443. \u0420\u0430\u0441\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u043f\u043e \u0433\u0440\u0443\u043f\u043f\u0430\u043c \u0431\u043b\u0438\u0437\u043a\u043e \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u043e\u043c\u0443 50\/50.<\/p>\n<pre><code class=\"bash\">&gt; python simulate_visits.py -n 1000  Moon\/Mars Exp Split: Mars: 488 visits (48.80%), Exact 50.00% Moon: 512 visits (51.20%), Exact 50.00%<\/code><\/pre>\n<p><a class=\"anchor\" name=\"%D0%A5%D1%8D%D1%88%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5\" id=\"\u0425\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\">\u0432\u0430\u043d\u0438\u0435&#187;<\/a><\/p>\n<h2>2. \u0425\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435<\/h2>\n<p>\u041a\u0430\u0436\u0434\u043e\u043c\u0443 \u043f\u043e\u0441\u0435\u0442\u0438\u0442\u0435\u043b\u044e \u043f\u0440\u0438\u0441\u0432\u0430\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0439 <code>device_id<\/code> \u0438 \u0437\u0430\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0432 \u043a\u0443\u043a\u0438. \u042d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430 \u0432\u044b\u0447\u0438\u0441\u043b\u044f\u0435\u0442\u0441\u044f \u043a\u0430\u043a <code>hash(device_id || experiment_name) % 2<\/code>.<\/p>\n<pre><code class=\"bash\">python 2_hash.py<\/code><\/pre>\n<p>\u042d\u043a\u0441\u043f: <a href=\"http:\/\/127.0.0.1:5000\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000<\/a><\/p>\n<pre><code class=\"python\">from flask import Flask, render_template_string, request, make_response import uuid import hashlib  app = Flask(__name__)  TEMPLATE = \"\"\" &lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt;     &lt;title&gt;A\/B Test&lt;\/title&gt;     &lt;link rel=\"stylesheet\" href=\"{{ url_for('static', filename='banners.css') }}\"&gt; &lt;\/head&gt; &lt;body&gt;     {% if variant == 'Moon' %}         &lt;div class=\"banner\" style=\"background-image: url('{{ url_for('static', filename='.\/moon.jpg') }}');\"&gt;             &lt;h1&gt;Walk on the Moon&lt;\/h1&gt;             &lt;div class=\"vspacer\"&gt;&lt;\/div&gt;             &lt;p&gt;Be one of the first tourists to set foot on the lunar surface. Your journey to another world starts here.&lt;\/p&gt;             &lt;button onclick=\"console.log('Click Moon')\"&gt;Reserve Your Spot&lt;\/button&gt;         &lt;\/div&gt;     {% else %}         &lt;div class=\"banner\" style=\"background-image: url('{{ url_for('static', filename='.\/mars.jpg') }}');\"&gt;             &lt;h1&gt;Journey to Mars&lt;\/h1&gt;             &lt;div class=\"vspacer\"&gt;&lt;\/div&gt;             &lt;p&gt;Be among the first humans to set foot on the Red Planet. Experience the adventure of a lifetime.&lt;\/p&gt;             &lt;button onclick=\"console.log('Click Mars')\"&gt;Reserve Your Spot&lt;\/button&gt;         &lt;\/div&gt;     {% endif %} &lt;\/body&gt; &lt;\/html&gt; \"\"\"  EXPERIMENT_NAME = \"moon_mars\"  def assign_group(device_id: str, experiment: str) -&gt; str:     key = f\"{device_id}:{experiment}\"     hash_bytes = hashlib.sha256(key.encode()).digest()     hash_int = int.from_bytes(hash_bytes, 'big')     return 'Moon' if hash_int % 2 == 0 else 'Mars'  @app.route('\/') def index():     device_id = request.cookies.get(\"device_id\")     if not device_id:         device_id = str(uuid.uuid4())     variant = assign_group(device_id, EXPERIMENT_NAME)     response = make_response(render_template_string(TEMPLATE, variant=variant))     response.set_cookie(\"device_id\", device_id, max_age=60*60*24*365)     return response  if __name__ == '__main__':     app.run(debug=True)<\/code><\/pre>\n<ul>\n<li>\n<p><code>device_id = str(uuid.uuid4())<\/code> &#8212; \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u0435\u0442 \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0439 ID \u043d\u043e\u0432\u044b\u043c \u043f\u043e\u0441\u0435\u0442\u0438\u0442\u0435\u043b\u044f\u043c.<\/p>\n<\/li>\n<li>\n<p><code>variant = assign_group(device_id, EXPERIMENT_NAME)<\/code> &#8212; \u0432\u044b\u0447\u0438\u0441\u043b\u044f\u0435\u0442 \u0433\u0440\u0443\u043f\u043f\u0443.<\/p>\n<\/li>\n<li>\n<p><code>key = f\"{device_id}:{experiment}\"<\/code> &#8212; \u043e\u0431\u044a\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 ID \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430 \u0434\u043b\u044f \u0432\u044b\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044f \u0433\u0440\u0443\u043f\u043f\u044b.<\/p>\n<\/li>\n<li>\n<p><code>response.set_cookie(\"device_id\", device_id, max_age=60*60*24*365)<\/code> &#8212; \u0437\u0430\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442 <code>device_id<\/code> \u0432 \u043a\u0443\u043a\u0438.<\/p>\n<\/li>\n<\/ul>\n<p>\u0420\u0430\u0441\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u043f\u043e \u0433\u0440\u0443\u043f\u043f\u0430\u043c \u0440\u0430\u0432\u043d\u043e\u043c\u0435\u0440\u043d\u043e\u0435.<\/p>\n<pre><code class=\"bash\">&gt; python simulate_visits.py -n 1000  Moon\/Mars Exp Split: Mars: 507 visits (50.70%), Exact 50.00% Moon: 493 visits (49.30%), Exact 50.00%<\/code><\/pre>\n<p><a class=\"anchor\" name=\"%D0%A4%D1%80%D0%BE%D0%BD%D1%82%D1%8D%D0%BD%D0%B4\" id=\"\u0424\u0440\u043e\u043d\u0442\u044d\u043d\u0434\">\u044d\u043d\u0434&#187;><\/a><\/p>\n<h2>3. \u0424\u0440\u043e\u043d\u0442\u044d\u043d\u0434<\/h2>\n<p>\u0424\u0440\u043e\u043d\u0442\u044d\u043d\u0434 \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u043e\u0431\u0435 \u0432\u0435\u0440\u0441\u0438\u0438 \u0438 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u0442 \u043d\u0443\u0436\u043d\u044b\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442. \u0413\u0440\u0443\u043f\u043f\u0430 \u0432\u044b\u0447\u0438\u0441\u043b\u044f\u0435\u0442\u0441\u044f \u043d\u0430 \u0431\u044d\u043a\u044d\u043d\u0434\u0435 \u0438 \u043f\u0435\u0440\u0435\u0434\u0430\u0451\u0442\u0441\u044f \u0432 \u043a\u0443\u043a\u0435 <code>\"exp_group\"<\/code>. \u0421 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0445\u0435\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0433\u0440\u0443\u043f\u043f\u0443 \u043c\u043e\u0436\u043d\u043e \u0432\u044b\u0447\u0438\u0441\u043b\u044f\u0442\u044c \u043d\u0430 \u0444\u0440\u043e\u043d\u0442\u044d\u043d\u0434\u0435 \u043f\u0440\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e\u043c <code>device_id<\/code>.<\/p>\n<pre><code class=\"bash\">python 3_frontend.py<\/code><\/pre>\n<p>\u042d\u043a\u0441\u043f: <a href=\"http:\/\/127.0.0.1:5000\" rel=\"noopener noreferrer nofollow\">http:\/\/127.0.0.1:5000<\/a><\/p>\n<pre><code class=\"python\">from flask import Flask, request, make_response, render_template_string import uuid import hashlib  app = Flask(__name__)  TEMPLATE = \"\"\" &lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt;     &lt;title&gt;A\/B Test&lt;\/title&gt;     &lt;link rel=\"stylesheet\" href=\"{{ url_for('static', filename='banners.css') }}\"&gt; &lt;\/head&gt; &lt;body&gt;     &lt;div id=\"variant-container\"&gt;Loading...&lt;\/div&gt;      &lt;script&gt;         function getCookie(name) {             const value = `; ${document.cookie}`;             const parts = value.split(`; ${name}=`);             if (parts.length === 2) return parts.pop().split(';').shift();         }          const expGroup = getCookie(\"exp_group\");         const container = document.getElementById(\"variant-container\");          if (expGroup === \"Moon\") {             container.innerHTML = `                 &lt;div class=\"banner\" style=\"background-image: url('{{ url_for('static', filename='.\/moon.jpg') }}');\"&gt;                     &lt;h1&gt;Walk on the Moon&lt;\/h1&gt;                     &lt;div class=\"vspacer\"&gt;&lt;\/div&gt;                     &lt;p&gt;Be one of the first tourists to set foot on the lunar surface. Your journey to another world starts here.&lt;\/p&gt;                     &lt;button onclick=\"console.log('Click Moon')\"&gt;Reserve Your Spot&lt;\/button&gt;                 &lt;\/div&gt;             `;         } else {             container.innerHTML = `                 &lt;div class=\"banner\" style=\"background-image: url('{{ url_for('static', filename='.\/mars.jpg') }}');\"&gt;                     &lt;h1&gt;Journey to Mars&lt;\/h1&gt;                     &lt;div class=\"vspacer\"&gt;&lt;\/div&gt;                     &lt;p&gt;Be among the first humans to set foot on the Red Planet. Experience the adventure of a lifetime.&lt;\/p&gt;                     &lt;button onclick=\"console.log('Click Mars')\"&gt;Reserve Your Spot&lt;\/button&gt;                 &lt;\/div&gt;             `;         }     &lt;\/script&gt; &lt;\/body&gt; &lt;\/html&gt; \"\"\"  EXPERIMENT_NAME = \"moon_mars\"  def assign_group(device_id: str, experiment: str) -&gt; str:     key = f\"{device_id}:{experiment}\"     hash_bytes = hashlib.sha256(key.encode()).digest()     hash_int = int.from_bytes(hash_bytes, 'big')     return 'Moon' if hash_int % 2 == 0 else 'Mars'  @app.route('\/') def index():     device_id = request.cookies.get(\"device_id\")     if not device_id:         device_id = str(uuid.uuid4())     variant = assign_group(device_id, EXPERIMENT_NAME)     response = make_response(render_template_string(TEMPLATE))     response.set_cookie(\"device_id\", device_id, max_age=60*60*24*365)     response.set_cookie(\"exp_group\", variant, max_age=60*60*24*365)     return response  if __name__ == '__main__':     app.run(debug=True)<\/code><\/pre>\n<ul>\n<li>\n<p><code>&lt;div id=\"variant-container\"&gt;Loading...&lt;\/div&gt;<\/code> &#8212; \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430.<\/p>\n<\/li>\n<li>\n<p><code>const expGroup = getCookie(\"exp_group\");<\/code> &#8212; \u0447\u0442\u0435\u043d\u0438\u0435 \u0433\u0440\u0443\u043f\u043f\u044b \u0438\u0437 \u043a\u0443\u043a.<\/p>\n<\/li>\n<li>\n<p><code>if (expGroup === \"Moon\") { container.innerHTML = ... }<\/code> &#8212; \u0437\u0430\u043c\u0435\u043d\u0430 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u043e\u043c, \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u043c \u0433\u0440\u0443\u043f\u043f\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f.<\/p>\n<\/li>\n<\/ul>\n<p>\u0414\u0435\u043b\u0435\u043d\u0438\u0435 \u0442\u0440\u0430\u0444\u0438\u043a\u0430 \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e\u0435.<\/p>\n<pre><code class=\"bash\">&gt; python simulate_visits.py -n 1000  Moon\/Mars Exp Split: Mars: 502 visits (50.20%), Exact 50.00% Moon: 498 visits (49.80%), Exact 50.00%<\/code><\/pre>\n<p><a class=\"anchor\" name=\"%D0%A1%D0%BE%D0%B1%D1%8B%D1%82%D0%B8%D1%8F\" id=\"\u0421\u043e\u0431\u044b\u0442\u0438\u044f\">\u0442\u0438\u044f&#187;<\/a><\/p>\n<h2>4. \u0421\u043e\u0431\u044b\u0442\u0438\u044f<\/h2>\n<p>\u041f\u0440\u0438 \u0437\u0430\u0445\u043e\u0434\u0435 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0438\u043b\u0438 \u043d\u0430\u0436\u0430\u0442\u0438\u0438 \u043a\u043d\u043e\u043f\u043a\u0438 \u0431\u0430\u043d\u043d\u0435\u0440\u0430 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0441\u043e\u0431\u044b\u0442\u0438\u044f <code><\/code><\/p>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-473243","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/473243","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=473243"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/473243\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=473243"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=473243"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=473243"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}