budget-optimizer


Namebudget-optimizer JSON
Version 0.0.4 PyPI version JSON
download
home_pagehttps://github.com/redam94/budget_optimizer
SummaryBudget optimizer for nested MMMs
upload_time2024-11-26 22:14:06
maintainerNone
docs_urlNone
authorMatthew Reda
requires_python>=3.10
licenseApache Software License 2.0
keywords mmm marketing budget optimizer kpi nested
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # budget_optimizer
Matthew Reda

<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

This library is to help wrap custom models for use in budget
optimization. It is designed to work with nested MMMs, where the budget
is allocated to different media channels and the performance is measured
by multiple KPIs which are fed into a downstream revenue model.

For example in a typical MMM, the budget is allocated to different media
channels and the performance is measured by sales, website visits, and
brand awareness. Website visits and brand awareness impact sales,
website visits are impacted by brand awareness. So the effects of
changing the budget flow through the nested KPIs.

The library is designed to work with any model that can be wrapped in a
`Model` class. The `Model` class should have needs a `predict` method
that takes a dictionary of parameters and returns an xarray dataset with
the model prediction for that model’s kpi. These model classes can be
composed in a `NestedModel` class which will flow the predictions into
the next stage of the model.

Model loading and functions to define how budget translates into model
inputs must be defined in a seperate file. Included in the folder with
the model artifacts in a file called `model_config.py` which should
contain the following functions:

- `model_loader` - a function that takes a path and returns a `Model`
  object
- `budget_to_model_inputs` - a function that a budget and model object
  and returns a dataset of model inputs

> [!NOTE]
>
> ### How to define the model_config.py file
>
> See the example in the `example_files` folder for an example of how to
> define these functions for a simple model.

## Developer Guide

If you are new to using `nbdev` here are some useful pointers to get you
started.

### Install budget_optimizer in Development mode

``` sh
# make sure budget_optimizer package is installed in development mode
$ pip install -e .

# make changes under nbs/ directory
# ...

# compile to have changes apply to budget_optimizer
$ nbdev_prepare
```

## Usage

### Installation

Install latest from the GitHub
[repository](https://github.com/redam94/budget_optimizer):

``` sh
$ pip install git+https://github.com/redam94/budget_optimizer.git
```

or from [pypi](https://pypi.org/project/budget_optimizer/)

``` sh
$ pip install budget_optimizer
```

### Documentation

Documentation can be found hosted on this GitHub
[repository](https://github.com/redam94/budget_optimizer)’s
[pages](https://redam94.github.io/budget_optimizer/). Additionally you
can find package manager specific guidelines on
[conda](https://anaconda.org/redam94/budget_optimizer) and
[pypi](https://pypi.org/project/budget_optimizer/) respectively.

## How to use

#### Step 1: Create a model_config.py file

This contains the functions to load the model and convert the budget
into model inputs. This allows models to be updated without changing the
code in the budget_optimizer library.

``` python
## file: example_files/model_1/model_config.py
import xarray as xr
from pathlib import Path
import numpy as np
from budget_optimizer.utils.model_helpers import AbstractModel, BudgetType

INITIAL_BUDGET: BudgetType = dict(a=2., b=3.)

class SimpleModel(AbstractModel):
  """
  Simple model that just adds the two variables a and b.
  This can be as complex as you want as long as it has a predict method
  that takes an xarray Dataset and returns an xarray DataArray and 
  a contributions method that takes an xarray Dataset and returns an xarray Dataset.
  
  Ideally, the model should also have data that defines the initial data that the
  model was trained on. You can wrap cutom models or functions in a class like this.
  """
  def __init__(self, data: xr.Dataset = None):
    self.data = data
    
  def predict(self, x: xr.Dataset) -> xr.DataArray:
    x = x.copy()
    x["prediction"] = np.exp(1 + .2*(x["a"]**2/(x["a"]**2 + np.exp(1)**2)) + .25*(x["b"]**4/(x["b"]**4 + np.exp(2)**4)))
    return x["prediction"]
  
  def contributions(self, x: xr.Dataset) -> xr.Dataset:
    return x

def budget_to_data(budget: BudgetType, model: AbstractModel) -> xr.Dataset:
    data = model.data.copy()
    for key, value in budget.items():
        data[key] = value/INITIAL_BUDGET[key]*data[key]
    return data
  
def model_loader(path: Path) -> AbstractModel:
    rng = np.random.default_rng(42)
    data_a = xr.DataArray(np.exp(1+rng.normal(0, .4, size=156)), dims='time', coords={"time": np.arange(1, 157)})
    data_b = xr.DataArray(np.exp(2+rng.normal(0, .2, size=156)), dims='time', coords={"time": np.arange(1, 157)})
    return SimpleModel(data = xr.Dataset({"a": data_a, "b": data_b}))
```

#### Step 2: Create a budget model

This is a class that wraps the model and defines how the budget is
allocated to the model inputs. It also tracks model names and kpis for
future use.

``` python
class RevenueModel(BaseBudgetModel):
    def __init__(self, model_name: str, model_kpi: str, model_path: str):
        super().__init__(model_name, model_kpi, model_path)
```

Initialize the model with the path to the model artifacts, model name,
and kpi name.

``` python
MODEL_NAME = "Revenue Model"
MODEL_KPI = "Revenue"
MODEL_PATH = "../example_files/model_1"
model = RevenueModel(MODEL_NAME, MODEL_KPI, MODEL_PATH)
budget_1 = dict(a=2, b=3)
budget_2 = dict(a=2.3, b=2.7)
outcome_budget_1 = model.predict(budget_1)
outcome_budget_2 = model.predict(budget_2)
```

We can now use the model to predict the kpi for a given budget.

<div id="fig-revenue-performance">

![](index_files/figure-commonmark/fig-revenue-performance-output-1.png)


Figure 1: Revenue Performance of Budget 1 and Budget 2

</div>

#### Step 3: Create the Optimizer Config Files

This is a file that defines the loss function for the optimization
problem. It should contain a function named `loss_fn` that takes the
predriction from the model and kwargs and returns a scalar loss to
minimize.

``` python
## file: example_files/optimizer_config.py
import numpy as np
import xarray as xr
from budget_optimizer.utils.model_helpers import BudgetType, load_yaml
from pathlib import Path

# Define the optimizer configuration
CONFIG = load_yaml(Path(__file__).parent / "optimizer_config.yaml")

def loss_fn(x: xr.DataArray, start_date=None, end_date=None, dim="Period"):
    # x is a numpy array of shape (n_params,)
    # start_date and end_date are datetime objects
    # return a scalar loss
    x = x.sel({dim: slice(start_date, end_date)})
    return -np.sum(x)

def optimizer_array_to_budget(array: np.ndarray) -> BudgetType:
    initial_budget: BudgetType = CONFIG['initial_budget']
    budget: BudgetType = {}
        
    for i, key in enumerate(initial_budget.keys()):
        budget[key] = array[i]
    return budget
```

An additional file will be used define the kwargs for the loss function
and the initial budget.

``` yaml
initial_budget:
  a: 2
  b: 3
loss_fn_kwargs:
  start_date: null
  end_date: null
  dim: "time"
```

#### Step 4: Create the Optimizer

Instantiate the optimizer and define the initial position, bounds and
constraints for the optimization problem.

``` python
init_budget = np.array([2, 3])
bounds = [(1.7, 2.3), (2.7, 3.3)]
constraints = opt.LinearConstraint([[1, 1]], [5], [5])
optimizer = Optimizer(model, "../example_files")
```

#### Step 5: Run the optimization

``` python
fitted_optimizer = optimizer.optimize(init_budget, bounds, constraints)
```

``` python
fitted_optimizer.optimal_budget
```

    {'a': np.float64(1.7002367944276708), 'b': np.float64(3.2997632055723294)}

<div id="fig-revenue-performance-optimized">

![](index_files/figure-commonmark/fig-revenue-performance-optimized-output-1.png)


Figure 2: Revenue Performance of Budget 1 and Budget 2

</div>

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/redam94/budget_optimizer",
    "name": "budget-optimizer",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.10",
    "maintainer_email": null,
    "keywords": "mmm marketing budget optimizer kpi nested",
    "author": "Matthew Reda",
    "author_email": "m.reda94@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/f3/ff/8a61d31dc6cee3a45eeebf5abf8d2102fdd2e5fa97547a80a2408e077d8a/budget_optimizer-0.0.4.tar.gz",
    "platform": null,
    "description": "# budget_optimizer\nMatthew Reda\n\n<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->\n\nThis library is to help wrap custom models for use in budget\noptimization. It is designed to work with nested MMMs, where the budget\nis allocated to different media channels and the performance is measured\nby multiple KPIs which are fed into a downstream revenue model.\n\nFor example in a typical MMM, the budget is allocated to different media\nchannels and the performance is measured by sales, website visits, and\nbrand awareness. Website visits and brand awareness impact sales,\nwebsite visits are impacted by brand awareness. So the effects of\nchanging the budget flow through the nested KPIs.\n\nThe library is designed to work with any model that can be wrapped in a\n`Model` class. The `Model` class should have needs a `predict` method\nthat takes a dictionary of parameters and returns an xarray dataset with\nthe model prediction for that model\u2019s kpi. These model classes can be\ncomposed in a `NestedModel` class which will flow the predictions into\nthe next stage of the model.\n\nModel loading and functions to define how budget translates into model\ninputs must be defined in a seperate file. Included in the folder with\nthe model artifacts in a file called `model_config.py` which should\ncontain the following functions:\n\n- `model_loader` - a function that takes a path and returns a `Model`\n  object\n- `budget_to_model_inputs` - a function that a budget and model object\n  and returns a dataset of model inputs\n\n> [!NOTE]\n>\n> ### How to define the model_config.py file\n>\n> See the example in the `example_files` folder for an example of how to\n> define these functions for a simple model.\n\n## Developer Guide\n\nIf you are new to using `nbdev` here are some useful pointers to get you\nstarted.\n\n### Install budget_optimizer in Development mode\n\n``` sh\n# make sure budget_optimizer package is installed in development mode\n$ pip install -e .\n\n# make changes under nbs/ directory\n# ...\n\n# compile to have changes apply to budget_optimizer\n$ nbdev_prepare\n```\n\n## Usage\n\n### Installation\n\nInstall latest from the GitHub\n[repository](https://github.com/redam94/budget_optimizer):\n\n``` sh\n$ pip install git+https://github.com/redam94/budget_optimizer.git\n```\n\nor from [pypi](https://pypi.org/project/budget_optimizer/)\n\n``` sh\n$ pip install budget_optimizer\n```\n\n### Documentation\n\nDocumentation can be found hosted on this GitHub\n[repository](https://github.com/redam94/budget_optimizer)\u2019s\n[pages](https://redam94.github.io/budget_optimizer/). Additionally you\ncan find package manager specific guidelines on\n[conda](https://anaconda.org/redam94/budget_optimizer) and\n[pypi](https://pypi.org/project/budget_optimizer/) respectively.\n\n## How to use\n\n#### Step 1: Create a model_config.py file\n\nThis contains the functions to load the model and convert the budget\ninto model inputs. This allows models to be updated without changing the\ncode in the budget_optimizer library.\n\n``` python\n## file: example_files/model_1/model_config.py\nimport xarray as xr\nfrom pathlib import Path\nimport numpy as np\nfrom budget_optimizer.utils.model_helpers import AbstractModel, BudgetType\n\nINITIAL_BUDGET: BudgetType = dict(a=2., b=3.)\n\nclass SimpleModel(AbstractModel):\n  \"\"\"\n  Simple model that just adds the two variables a and b.\n  This can be as complex as you want as long as it has a predict method\n  that takes an xarray Dataset and returns an xarray DataArray and \n  a contributions method that takes an xarray Dataset and returns an xarray Dataset.\n  \n  Ideally, the model should also have data that defines the initial data that the\n  model was trained on. You can wrap cutom models or functions in a class like this.\n  \"\"\"\n  def __init__(self, data: xr.Dataset = None):\n    self.data = data\n    \n  def predict(self, x: xr.Dataset) -> xr.DataArray:\n    x = x.copy()\n    x[\"prediction\"] = np.exp(1 + .2*(x[\"a\"]**2/(x[\"a\"]**2 + np.exp(1)**2)) + .25*(x[\"b\"]**4/(x[\"b\"]**4 + np.exp(2)**4)))\n    return x[\"prediction\"]\n  \n  def contributions(self, x: xr.Dataset) -> xr.Dataset:\n    return x\n\ndef budget_to_data(budget: BudgetType, model: AbstractModel) -> xr.Dataset:\n    data = model.data.copy()\n    for key, value in budget.items():\n        data[key] = value/INITIAL_BUDGET[key]*data[key]\n    return data\n  \ndef model_loader(path: Path) -> AbstractModel:\n    rng = np.random.default_rng(42)\n    data_a = xr.DataArray(np.exp(1+rng.normal(0, .4, size=156)), dims='time', coords={\"time\": np.arange(1, 157)})\n    data_b = xr.DataArray(np.exp(2+rng.normal(0, .2, size=156)), dims='time', coords={\"time\": np.arange(1, 157)})\n    return SimpleModel(data = xr.Dataset({\"a\": data_a, \"b\": data_b}))\n```\n\n#### Step 2: Create a budget model\n\nThis is a class that wraps the model and defines how the budget is\nallocated to the model inputs. It also tracks model names and kpis for\nfuture use.\n\n``` python\nclass RevenueModel(BaseBudgetModel):\n    def __init__(self, model_name: str, model_kpi: str, model_path: str):\n        super().__init__(model_name, model_kpi, model_path)\n```\n\nInitialize the model with the path to the model artifacts, model name,\nand kpi name.\n\n``` python\nMODEL_NAME = \"Revenue Model\"\nMODEL_KPI = \"Revenue\"\nMODEL_PATH = \"../example_files/model_1\"\nmodel = RevenueModel(MODEL_NAME, MODEL_KPI, MODEL_PATH)\nbudget_1 = dict(a=2, b=3)\nbudget_2 = dict(a=2.3, b=2.7)\noutcome_budget_1 = model.predict(budget_1)\noutcome_budget_2 = model.predict(budget_2)\n```\n\nWe can now use the model to predict the kpi for a given budget.\n\n<div id=\"fig-revenue-performance\">\n\n![](index_files/figure-commonmark/fig-revenue-performance-output-1.png)\n\n\nFigure\u00a01: Revenue Performance of Budget 1 and Budget 2\n\n</div>\n\n#### Step 3: Create the Optimizer Config Files\n\nThis is a file that defines the loss function for the optimization\nproblem. It should contain a function named `loss_fn` that takes the\npredriction from the model and kwargs and returns a scalar loss to\nminimize.\n\n``` python\n## file: example_files/optimizer_config.py\nimport numpy as np\nimport xarray as xr\nfrom budget_optimizer.utils.model_helpers import BudgetType, load_yaml\nfrom pathlib import Path\n\n# Define the optimizer configuration\nCONFIG = load_yaml(Path(__file__).parent / \"optimizer_config.yaml\")\n\ndef loss_fn(x: xr.DataArray, start_date=None, end_date=None, dim=\"Period\"):\n    # x is a numpy array of shape (n_params,)\n    # start_date and end_date are datetime objects\n    # return a scalar loss\n    x = x.sel({dim: slice(start_date, end_date)})\n    return -np.sum(x)\n\ndef optimizer_array_to_budget(array: np.ndarray) -> BudgetType:\n    initial_budget: BudgetType = CONFIG['initial_budget']\n    budget: BudgetType = {}\n        \n    for i, key in enumerate(initial_budget.keys()):\n        budget[key] = array[i]\n    return budget\n```\n\nAn additional file will be used define the kwargs for the loss function\nand the initial budget.\n\n``` yaml\ninitial_budget:\n  a: 2\n  b: 3\nloss_fn_kwargs:\n  start_date: null\n  end_date: null\n  dim: \"time\"\n```\n\n#### Step 4: Create the Optimizer\n\nInstantiate the optimizer and define the initial position, bounds and\nconstraints for the optimization problem.\n\n``` python\ninit_budget = np.array([2, 3])\nbounds = [(1.7, 2.3), (2.7, 3.3)]\nconstraints = opt.LinearConstraint([[1, 1]], [5], [5])\noptimizer = Optimizer(model, \"../example_files\")\n```\n\n#### Step 5: Run the optimization\n\n``` python\nfitted_optimizer = optimizer.optimize(init_budget, bounds, constraints)\n```\n\n``` python\nfitted_optimizer.optimal_budget\n```\n\n    {'a': np.float64(1.7002367944276708), 'b': np.float64(3.2997632055723294)}\n\n<div id=\"fig-revenue-performance-optimized\">\n\n![](index_files/figure-commonmark/fig-revenue-performance-optimized-output-1.png)\n\n\nFigure\u00a02: Revenue Performance of Budget 1 and Budget 2\n\n</div>\n",
    "bugtrack_url": null,
    "license": "Apache Software License 2.0",
    "summary": "Budget optimizer for nested MMMs",
    "version": "0.0.4",
    "project_urls": {
        "Homepage": "https://github.com/redam94/budget_optimizer"
    },
    "split_keywords": [
        "mmm",
        "marketing",
        "budget",
        "optimizer",
        "kpi",
        "nested"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "d87d925a489c674650c1bb1454b6dc012ba4f823e6ea30198482688761d323f6",
                "md5": "52c891ee6a4fc80073a3af44997597e0",
                "sha256": "29c8e5a275704f0a6b9cad4492cc342fe0fbe7603c4e33faf1d40111e406d522"
            },
            "downloads": -1,
            "filename": "budget_optimizer-0.0.4-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "52c891ee6a4fc80073a3af44997597e0",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.10",
            "size": 13932,
            "upload_time": "2024-11-26T22:14:04",
            "upload_time_iso_8601": "2024-11-26T22:14:04.803609Z",
            "url": "https://files.pythonhosted.org/packages/d8/7d/925a489c674650c1bb1454b6dc012ba4f823e6ea30198482688761d323f6/budget_optimizer-0.0.4-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "f3ff8a61d31dc6cee3a45eeebf5abf8d2102fdd2e5fa97547a80a2408e077d8a",
                "md5": "990999c5ab9434009973a3e25c5f8056",
                "sha256": "41b85470491b55d2418cb60cffbd53b6fd66621efe43866c42bde299fa020ba4"
            },
            "downloads": -1,
            "filename": "budget_optimizer-0.0.4.tar.gz",
            "has_sig": false,
            "md5_digest": "990999c5ab9434009973a3e25c5f8056",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.10",
            "size": 15921,
            "upload_time": "2024-11-26T22:14:06",
            "upload_time_iso_8601": "2024-11-26T22:14:06.775155Z",
            "url": "https://files.pythonhosted.org/packages/f3/ff/8a61d31dc6cee3a45eeebf5abf8d2102fdd2e5fa97547a80a2408e077d8a/budget_optimizer-0.0.4.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-11-26 22:14:06",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "redam94",
    "github_project": "budget_optimizer",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "budget-optimizer"
}
        
Elapsed time: 1.20149s