Skip to content

Basic example with dataframes

The previous example would quickly become unworkable if we had more than just tofu and chickpeas. I'll walk you through how we can make a food dimension to make this scalable. You can also skip to the end to see the complete example.

Note that instead of hardcoding values, we'll be reading from the following csv file.

food_data.csv

food protein cost
tofu_block 18 4
chickpea_can 15 3

Load your data

Nothing special here. Load your data using your favourite dataframe library. We like Polars because it's fast but Pandas works too.

import polars as pl

data = pl.read_csv("food_data.csv")
import pandas as pd

data = pd.read_csv("food_data.csv")

Create the model

import pyoframe as pf

m = pf.Model()

Create an dimensioned variable

Previously, we created two variables: m.tofu_blocks and m.chickpea_cans. Instead, we now create a single variable dimensioned over food.

m.Buy = pf.Variable(data[["food"]], lb=0, vtype="integer")

Printing the variable shows that it contains a food dimension with indices tofu and chickpeas!

>>> m.Buy
<Variable name=Buy lb=0 size=2 dimensions={'food': 2}>
[tofu_block]: Buy[tofu_block]
[chickpea_can]: Buy[chickpea_can]

Variable naming

We suggest capitalizing model variables (i.e. m.Buy not m.buy) to make it easy to distinguish what is and isn't a variable.

Create the objective

Previously we had:

m.minimize = 4 * m.tofu_blocks + 3 * m.chickpea_cans

How do we make use of our dimensioned variable m.Buy instead?

First, we multiply the variable by the protein amount.

>>> data[["food", "cost"]] * m.Buy
<Expression size=2 dimensions={'food': 2} terms=2>
[tofu_block]: 4 Buy[tofu_block]
[chickpea_can]: 3 Buy[chickpea_can]

As you can see, Pyoframe with a bit of magic converted our Variable into an Expression where the coefficients are the protein amounts.

Second, notice that our Expression still has a food dimension—it really contains two separate expressions, one for tofu and one for chickpeas. All objective functions must be a single expression (without dimensions) so let's sum over the food dimensions using pf.sum().

>>> pf.sum("food", data[["food", "cost"]] * m.Buy)
<Expression size=1 dimensions={} terms=2>
4 Buy[tofu_block] +3 Buy[chickpea_can]

This works and since food is the only dimensions we don't even need to specify it. Putting it all together:

m.minimize = pf.sum(data[["food", "cost"]] * m.Buy)

Adding the constraint

This is similar to how we created the objective, except now we're using protein and we turn our Expression into a Constraint by with the >= operation.

m.protein_constraint = pf.sum(data[["food", "protein"]] * m.Buy) >= 50

Putting it all together

import pandas as pd
import pyoframe as pf

data = pd.read_csv("food_data.csv")

m = pf.Model()
m.Buy = pf.Variable(data[["food"]], lb=0, vtype="integer")
m.minimize = pf.sum(data[["food", "cost"]] * m.Buy)
m.protein_constraint = pf.sum(data[["food", "protein"]] * m.Buy) >= 50

m.optimize()

So you should buy:

>>> m.Buy.solution
┌──────────────┬──────────┐
│ food         ┆ solution │
│ ---          ┆ ---      │
│ str          ┆ i64      │
╞══════════════╪══════════╡
│ tofu_block   ┆ 2        │
│ chickpea_can ┆ 1        │
└──────────────┴──────────┘

Notice that since m.Buy is dimensioned, m.Buy.solution returned a dataframe with the solution for each of indices.

Returning Pandas dataframes

Pyoframe currently always returns Polars dataframes but you can easily convert them to Pandas using .to_pandas(). In the future, we plan to add support for automatically returning Pandas dataframes. Upvote the issue if you'd like this feature.