Usage

This page gives a brief run through of methods used to build optimization models in gurobipy-pandas and extract solutions. gurobipy-pandas provides several free functions for model building, and implements pandas’ accessors to facilitate building models and querying results from dataframes and series respectively.

Standard Imports

Most gurobipy-pandas applications will start with the following imports.

>>> import pandas as pd
>>> import gurobipy as gp
>>> from gurobipy import GRB
>>> import gurobipy_pandas as gppd

Interactive Mode

When working in an interactive environment (such as the IPython shell or a Jupyter notebook) it can be helpful to use interactive mode. This makes it easier to inspect changes to an optimization model as you are building it. There is a performance hit for doing this, so while most examples in this documentation use interactive mode, you should in general remove it for production applications where speed is a concern. The following command enables interactive mode:

>>> gppd.set_interactive()

Creating a Model

>>> model = gp.Model()

Creating Variables

In gurobipy-pandas, variables are always created aligned with an index. To create a Series of variables, use gppd.add_vars. Note that you can pass the name argument to automatically construct names based on the index of the input DataFrame, and set other attributes such as upper bounds by referencing columns in the input DataFrame.

>>> data = pd.DataFrame(
...     {
...         "i": [0, 0, 1, 2, 2],
...         "j": [1, 2, 0, 0, 1],
...         "u": [0.3, 1.2, 0.7, 0.9, 1.2],
...         "c": [1.3, 1.7, 1.4, 1.1, 0.9],
...     }
... ).set_index(["i", "j"])
>>> data  
       u    c
i j
0 1  0.3  1.3
  2  1.2  1.7
1 0  0.7  1.4
2 0  0.9  1.1
  1  1.2  0.9
>>> x = gppd.add_vars(model, data, name="x", ub="u")
>>> x
i  j
0  1    <gurobi.Var x[0,1]>
   2    <gurobi.Var x[0,2]>
1  0    <gurobi.Var x[1,0]>
2  0    <gurobi.Var x[2,0]>
   1    <gurobi.Var x[2,1]>
Name: x, dtype: object

You can also use the DataFrame gppd accessor to create variables. The distinction between the two methods is that the accessor returns a new DataFrame with variables appended as new columns, allowing method chaining.

>>> variables = (
...     data.gppd.add_vars(model, name="y")
...     .gppd.add_vars(model, name="z")
... )
>>> variables  
       u    c                    y                    z
i j
0 1  0.3  1.3  <gurobi.Var y[0,1]>  <gurobi.Var z[0,1]>
  2  1.2  1.7  <gurobi.Var y[0,2]>  <gurobi.Var z[0,2]>
1 0  0.7  1.4  <gurobi.Var y[1,0]>  <gurobi.Var z[1,0]>
2 0  0.9  1.1  <gurobi.Var y[2,0]>  <gurobi.Var z[2,0]>
  1  1.2  0.9  <gurobi.Var y[2,1]>  <gurobi.Var z[2,1]>

Arithmetic Expressions

Building linear and quadratic expressions from variables is handled using standard pandas methods. For example, you can use arithmetic operations to create relationships across rows:

>>> variables["y"] + variables["z"]
i  j
0  1    y[0,1] + z[0,1]
   2    y[0,2] + z[0,2]
1  0    y[1,0] + z[1,0]
2  0    y[2,0] + z[2,0]
   1    y[2,1] + z[2,1]
dtype: object

And you can use groupby and aggregate to build summations across different levels of an index:

>>> x.groupby("i").sum()
i
0        x[0,1] + x[0,2]
1    <gurobi.Var x[1,0]>
2        x[2,0] + x[2,1]
Name: x, dtype: object
>>> x.groupby("j").sum()
j
0        x[1,0] + x[2,0]
1        x[0,1] + x[2,1]
2    <gurobi.Var x[0,2]>
Name: x, dtype: object

Note that the builtin .sum in pandas can be slow when working with a huge number of gurobipy modelling objects. In such applications, you should use .agg(gp.quicksum) instead. See the performance section of the documentation for further details.

Adding Constraints

Constraints are added row-wise. Similarly to adding variables, you have the option of using a free function or a dataframe accessor. The free function accepts series aligned on the same index to construct constraints, returning new constraints as a series. The following expresses the constraint \(x \le y\) for each entry in the index:

>>> gppd.add_constrs(  
...     model,
...     variables.groupby("j")["y"].sum(),
...     GRB.LESS_EQUAL,
...     variables.groupby("i")["y"].sum(),
...     name="c1",
... )
0    <gurobi.Constr c1[0]>
1    <gurobi.Constr c1[1]>
2    <gurobi.Constr c1[2]>
Name: c1, dtype: object

While the dataframe accessor takes column name references to build constraints:

>>> variables.gppd.add_constrs(  
...     model, "y", GRB.LESS_EQUAL, "z", name="c1"
... )
       u    c                    y                    z                       c1
i j
0 1  0.3  1.3  <gurobi.Var y[0,1]>  <gurobi.Var z[0,1]>  <gurobi.Constr c1[0,1]>
  2  1.2  1.7  <gurobi.Var y[0,2]>  <gurobi.Var z[0,2]>  <gurobi.Constr c1[0,2]>
1 0  0.7  1.4  <gurobi.Var y[1,0]>  <gurobi.Var z[1,0]>  <gurobi.Constr c1[1,0]>
2 0  0.9  1.1  <gurobi.Var y[2,0]>  <gurobi.Var z[2,0]>  <gurobi.Constr c1[2,0]>
  1  1.2  0.9  <gurobi.Var y[2,1]>  <gurobi.Var z[2,1]>  <gurobi.Constr c1[2,1]>

You can also use a string syntax similar to pandas’ eval method to build the same constraint concisely:

>>> variables.gppd.add_constrs(  
...     model, "y + z <= 1", name="c1"
... )
       u    c                    y                    z                       c1
i j
0 1  0.3  1.3  <gurobi.Var y[0,1]>  <gurobi.Var z[0,1]>  <gurobi.Constr c1[0,1]>
  2  1.2  1.7  <gurobi.Var y[0,2]>  <gurobi.Var z[0,2]>  <gurobi.Constr c1[0,2]>
1 0  0.7  1.4  <gurobi.Var y[1,0]>  <gurobi.Var z[1,0]>  <gurobi.Constr c1[1,0]>
2 0  0.9  1.1  <gurobi.Var y[2,0]>  <gurobi.Var z[2,0]>  <gurobi.Constr c1[2,0]>
  1  1.2  0.9  <gurobi.Var y[2,1]>  <gurobi.Var z[2,1]>  <gurobi.Constr c1[2,1]>

Note that you must correctly align all data, and fill values when necessary, when adding constraints. Missing data is not allowed and will throw an error. This is by design, as misaligned data and variables likely indicates a bug in model building logic.

gurobipy methods

In some cases, you will need to call gurobipy methods directly, using expressions produced from pandas series or dataframes. A common example is setting an objective, since this is not done per-row but from a single expression.

>>> (x * data["c"]).sum()
<gurobi.LinExpr: 1.3 x[0,1] + 1.7 x[0,2] + 1.4 x[1,0] + 1.1 x[2,0] + 0.9 x[2,1]>
>>> model.setObjective((x * data["c"]).sum(), sense=GRB.MAXIMIZE)

Solving the model

>>> model.optimize()  
Gurobi Optimizer version...
...
Optimal objective  5.480000000e+00

Extracting solutions

Variable values in the optimal solution can be extracted using the series accessor.

>>> x.gppd.X
i  j
0  1    0.3
   2    1.2
1  0    0.7
2  0    0.9
   1    1.2
Name: x, dtype: float64