{"id":14974188,"url":"https://github.com/deacondesperado/flask_mab","last_synced_at":"2025-10-29T01:41:04.785Z","repository":{"id":11465694,"uuid":"13930983","full_name":"DeaconDesperado/flask_mab","owner":"DeaconDesperado","description":"An implementation of the multi-armed bandit optimization pattern as a Flask extension","archived":false,"fork":false,"pushed_at":"2025-03-31T01:41:46.000Z","size":422,"stargazers_count":81,"open_issues_count":8,"forks_count":12,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-05T00:11:53.268Z","etag":null,"topics":["flask","flask-extension","python"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/DeaconDesperado.png","metadata":{"files":{"readme":"README.rst","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":"docs/roadmap.rst","authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2013-10-28T16:09:47.000Z","updated_at":"2025-03-10T17:46:38.000Z","dependencies_parsed_at":"2024-05-06T02:42:15.352Z","dependency_job_id":"f61afac6-8f41-40d8-ad7d-803905b565a6","html_url":"https://github.com/DeaconDesperado/flask_mab","commit_stats":{"total_commits":173,"total_committers":6,"mean_commits":"28.833333333333332","dds":0.4393063583815029,"last_synced_commit":"a42c83d731d0a6ef5947bfc6ffafa755ad0e8327"},"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DeaconDesperado%2Fflask_mab","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DeaconDesperado%2Fflask_mab/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DeaconDesperado%2Fflask_mab/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DeaconDesperado%2Fflask_mab/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/DeaconDesperado","download_url":"https://codeload.github.com/DeaconDesperado/flask_mab/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247276176,"owners_count":20912288,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["flask","flask-extension","python"],"created_at":"2024-09-24T13:50:07.670Z","updated_at":"2025-10-29T01:41:04.713Z","avatar_url":"https://github.com/DeaconDesperado.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"Flask-MAB\n=========\n\n.. image:: https://travis-ci.org/DeaconDesperado/flask_mab.png?:target: https://travis-ci.org/DeaconDesperado/flask_mab\n\nFlask-MAB is an implementation of multi-armed bandit test pattern as a flask middleware.\n\nIt can be used to test the effectiveness of virtually any parts of your app using user signals.\n\nIf you can pass it, we can test it!\n\nNote for users of pre-release version:  The API has [changed](https://github.com/DeaconDesperado/flask_mab/issues/2) significantly with 1.0 to better fit with the [application factory pattern](http://flask.pocoo.org/docs/patterns/appfactories/).\n\n`Complete Documentation \u003chttp://pythonhosted.org/Flask-MAB/\u003e`_.\n\nMulti-armed what?!\n------------------\n\nA multi-armed bandit is essentially an online alternative to classical A/B testing.  Whereas \nA/B testing is generally split into extended phases of execution and analysis, Bandit algorithms\ncontinually adjust to user feedback and optimize between experimental states.  Bandits typically\nrequire very little curation and can in fact be left running indefinitely if need be.\n\nThe curious sounding name is drawn from the \"one-armed bandit\", an colloquialism for casino\nslot machines.  Bandit algorithms can be thought of along similar lines as a eager slot player:\nif one were to play many slot machines continously over many thousands of attempts, one would eventually\nbe able to determine which machines were hotter than others.  A multi-armed bandit is merely an algorithm \nthat performs exactly this determination, using your user's interaction as its \"arm pulls\".  Extracting winning\npatterns becomes a fluid part of interacting with the application.\n\nWhile bandit algorithms can provide excellent automated optimization, it's important to note that they are not considered a replacement for classic A/B tests.  Bandits could be considered a sort of \"black box,\" in the sense that their intuitions become opaque as they optimize.  Experiments that call for rigorous tests of statistical significance may be better suited to more traditional frameworks.\n\nJohn Myles White has an awesome treatise on Bandit implementations in his book `Bandit Algorithms for Website Optimization \u003chttp://shop.oreilly.com/product/0636920027393.do\u003e`_.  Most of the code in this library consistes of his excellent guidelines reimplemented to suit the nature of the Flask request lifecycle.\n\nGetting Started\n===============\n\nTo get started defining experiments, there several steps:\n\n#. Determine what parts of your app you'd like to optimize\n#. Setup a storage engine (currently only json, though mongo+zodb are in the roadmap)\n#. Instantiate Bandits for all your experiments (you can have as many as you like, several experiments\n   can run at once in a single app.)\n#. Assign arms to your bandits that represent your experimental states\n#. Attach the BanditMiddleware to your Flask app.\n\nThis guide will take you through each step.  The example case we'll be working with is included in the source under the\n'example' folder if you'd like to try running the finished product.\n\nDetermining what to test\n------------------------\n\nThe first task at hand requires a little planning.  What are some of the things in your app you've always\nbeen curious about changing, but never had empirical data to back up potential modifications?  Bandits are best\nsuited to cases where changes can be \"slipped in\" without the user noticing, but since the state assigned to a user\nwill be persisted to their client, you can also change things like UI.\n\nFor our example case, we'll be changing the label text and color of a button in our app to see if either change increases\nuser interaction with the feature.  We'll be representing these states as two separate experiements (so a user will get separate\nassignments for color and text) but you could conceviably make them one experiment by utilizing a tuple or sequence.  More on that later!\n\nSetting up your storage backend\n--------------------------------\n\nHTTP itself is stateless, but bandits need to persist their increments between requests.  In order to accomplish this, there is a \nbandit storage interface that can be implemented to save all the experiments for an application down to memory, database, etc.\n\nAt present, the only core implementation of this interface saves the bandits down to a JSON file at the path you specify, but this should\nwork for most purposes.  For 1.0 release, implementations using MongoDB and ZODB are planned.\n\nStorage engines are attached using flask configuration directives.\n\nLet's start setting up our bandit file storage::\n\n    app.config['MAB_STORAGE_ENGINE'] = 'JSONBanditStorage'\n    app.config['MAB_STORAGE_OPTS'] = ('./example/bandit_storage.json',) \n\nThis storage instance will be passed into our bandit middleware and all values that need to be persisted will be handled under the hood.\n\nThe storage opts are just arguments to be passed to the storage instance constructor (in this case, just the path to a flat file to store the information.)\n\nCreate bandits and assigning arms\n---------------------------------\n\nThe next step is to create a bandit for each experiment we want to test.\n\nThere are several different bandit implemenations included, but for the purposes of this example we'll be using an bandits.EpsilonGreedyBandit,\nan algorithm which aggressively assigns the present winner according to a fixed constant value, epsilon\n\nExpanding upon our previous example, here are our bandits alongside our storage engine::\n\n    from flask.ext.mab.storage import JSONBanditStorage\n    from flask.ext.mab.bandits import EpsilonGreedyBandit\n\n    color_bandit = EpsilonGreedyBandit(0.2)\n    color_bandit.add_arm(\"green\",\"#00FF00\")\n    color_bandit.add_arm(\"red\",\"#FF0000\")\n    color_bandit.add_arm(\"blue\",\"#0000FF\")\n\n    txt_bandit = EpsilonGreedyBandit(0.5)\n    txt_bandit.add_arm(\"casual\",\"Hey dude, wanna buy me?\")\n    txt_bandit.add_arm(\"neutral\",\"Add to cart\")\n    txt_bandit.add_arm(\"formal\",\"Good day sir... care to purchase?\")\n\nHere we have two bandits, one of which will randomize %20 of the time on the color of the button, the other %50 of the time on the text.  The colors and\ntest blurbs are considered our \"arms\" in the bandit parlance.  An epsilon greedy bandit splits states between random selection and deterministically \nselecting the \"winner\", so as users click more, thereby sending reward signals, one combination of these two states will start to win out.\n\nThis code could easily be refactored using a function or generator, but for now, we'll include the full boilerplate.  If you have a lot of experiments, consider \ndefining a function to be more convenient.\n\nAttaching the middleware\n------------------------\n\nThe main BanditMiddleware is where all the magic happens.  Attaching it to our app, assigning it some bandits, and sending it pull and reward \nsignals is all that's necessary to get the test going.\n\nExpanding on our example, we'll define a simple flask app with some basic routes for rendering the interface.  These routes will also understand how to reward the right\narms and update the bandits so the state of the experiment starts adjusting in realtime.\n\nAgain, boilerplate here could be easily cut down, but here is a rough example::\n\n    from flask import Flask,render_template\n    from flask.ext.mab import BanditMiddleware\n\n    app = Flask('test_app')\n    mab = BanditMiddleware() \n    mab.init_app(app)\n    app.add_bandit('color_btn',color_bandit) #our bandits from previous code block\n    app.add_bandit('txt_btn',txt_bandit)\n\n    @app.route(\"/\")\n    def home():\n        \"\"\"Render the btn\"\"\"\n        return render_template(\"ui.html\")\n\n    @app.route(\"/btnclick\")\n    def home():\n        \"\"\"Button was clicked!\"\"\"\n        return render_template(\"btnclick.html\")\n\nNow our app understands that it should be tracking two experiments and persisting their values to a file.  \"Arms\" that get selected for every \nuser will be persisted to cookies.  However, we still need to make the system understand what endpoints use which experiments.  In our example case,\nthe \"/\" route is going to render the button, and so both states will need to be assigned there.  The \"/btnclick\" endpoint, alternatively, is where our \nreward is determined, the theoretical \"payoff\" that state won us.  In this case, its a boolean, assigning a 1 if the button gets clicked.  So how are these\ntwo signals sent to the middleware?  There are decorators much like the route decorator that easily registers these actions.\n\nUsing the decorators\n++++++++++++++++++++\n\nSetting up the MAB feedback cycle is easily negotiated by endpoint::\n\n    @app.route(\"/\")\n    @mab.choose_arm(\"color_btn\")\n    @mab.choose_arm(\"txt_btn\")\n    def home(color_btn, txt_btn):\n        \"\"\"Render the btn using values from the bandit\"\"\"\n        return render_template(\"ui.html\",btn_color=color_btn,btn_text=txt_btn)\n\n    @app.route(\"/btnclick\")\n    @mab.reward_endpt(\"color_btn\",1.0)\n    @mab.reward_endpt(\"txt_btn\",1.0)\n    def reward():\n        \"\"\"Button was clicked!\"\"\"\n        return render_template(\"btnclick.html\")\n\nUsing these decorators, our middleware knows that the it should suggest some values for both our experiments at the root endpoint.  When decorating with choose_arm, we identify the bandit/experiment we need a value assignment for.  Just like parameters from your route **these values are passed into the view function in the order you decorated for them, always after your route params** \n\nIt should be stressed that things like colors are probably best stored in CSS, but for this example we'll pass the values right into jinja.  You could consider setting up a \ndedicated endpoint for experiments with static styles like this, one that could parse and render your CSS.  The rough idea here is to leave what the bandit actually affects up to you.\n\nOn the other side of the process, our \"/btnclick\" endpoint now knows that whatever \"arms\" assigned to this user worked out well, because the user clicked it.  The \nBanditMiddleware.reward_endpt decorator knows to look in our user's cookie for the values that were assigned to her and give them some props.  We're using\nbooleans here, but you could pass any amount of reward in the event that some states in your experiment are better than others (you could for example weight your experiments differently.)\n\nThat's it!  This user's feedback will be persisted by the middleware and used to adjust the content for future users.  Over time, this pattern will start converging to a winner.\nYour app will get optimized on these two experimental features for free!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeacondesperado%2Fflask_mab","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdeacondesperado%2Fflask_mab","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeacondesperado%2Fflask_mab/lists"}