Skip to content

Commit a98686d

Browse files
committed
feat: add helper functions to enable dataframe exporting
1 parent 6b76173 commit a98686d

4 files changed

Lines changed: 126 additions & 0 deletions

File tree

bindings/python/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,44 @@ model.scenario("baseline", log_to_console=False)
6969
solution = model.run_scenario("baseline")
7070
assert solution.is_optimal()
7171
```
72+
73+
## Result Export To pandas/polars DataFrames
74+
75+
Solve results can be exported directly to DataFrames for analysis workflows.
76+
77+
- `solution.to_pandas(table="variables")`
78+
- `solution.to_polars(table="variables")`
79+
80+
Supported `table` values are:
81+
82+
- `"variables"`: one row per variable with primal value and reduced cost.
83+
- `"constraints"`: one row per constraint with dual value.
84+
- `"summary"`: one-row solve summary (status, objective, solve time).
85+
86+
Example:
87+
88+
```python
89+
solution = model.run_scenario("baseline")
90+
91+
variables_df = solution.to_pandas(table="variables")
92+
constraints_df = solution.to_pandas(table="constraints")
93+
summary_df = solution.to_pandas(table="summary")
94+
95+
variables_pl = solution.to_polars(table="variables")
96+
```
97+
98+
Install optional dependencies as needed:
99+
100+
```bash
101+
cd bindings/python
102+
uv pip install pandas
103+
uv pip install polars
104+
```
105+
106+
or
107+
108+
```bash
109+
cd bindings/python
110+
uv add pandas
111+
uv add polars
112+
```

bindings/python/arco/__init__.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,9 +200,80 @@ def _model_run_scenario(
200200
return self.solve(**solve_kwargs)
201201

202202

203+
def _solve_result_records(
204+
self: _arco.SolveResult,
205+
*,
206+
table: str = "variables",
207+
) -> list[dict[str, object]]:
208+
if table == "variables":
209+
return [
210+
{
211+
"variable_id": idx,
212+
"value": primal,
213+
"reduced_cost": reduced_cost,
214+
}
215+
for idx, (primal, reduced_cost) in enumerate(
216+
zip(self.primal_values, self.variable_duals, strict=True)
217+
)
218+
]
219+
220+
if table == "constraints":
221+
return [
222+
{
223+
"constraint_id": idx,
224+
"dual": dual,
225+
}
226+
for idx, dual in enumerate(self.constraint_duals)
227+
]
228+
229+
if table == "summary":
230+
return [
231+
{
232+
"status": self.status_string(),
233+
"objective_value": self.objective_value,
234+
"solve_time_seconds": self.solve_time_seconds(),
235+
"is_optimal": self.is_optimal(),
236+
}
237+
]
238+
239+
raise ValueError("table must be one of {'variables', 'constraints', 'summary'}")
240+
241+
242+
def _solve_result_to_pandas(
243+
self: _arco.SolveResult,
244+
*,
245+
table: str = "variables",
246+
) -> object:
247+
try:
248+
import pandas as pd
249+
except ModuleNotFoundError as exc: # pragma: no cover - optional dependency
250+
raise ModuleNotFoundError(
251+
"to_pandas() requires pandas. Install with `uv add pandas` or `pip install pandas`."
252+
) from exc
253+
254+
return pd.DataFrame.from_records(_solve_result_records(self, table=table))
255+
256+
257+
def _solve_result_to_polars(
258+
self: _arco.SolveResult,
259+
*,
260+
table: str = "variables",
261+
) -> object:
262+
try:
263+
import polars as pl
264+
except ModuleNotFoundError as exc: # pragma: no cover - optional dependency
265+
raise ModuleNotFoundError(
266+
"to_polars() requires polars. Install with `uv add polars` or `pip install polars`."
267+
) from exc
268+
269+
return pl.DataFrame(_solve_result_records(self, table=table))
270+
271+
203272
setattr(_arco.Model, "control", _model_control)
204273
setattr(_arco.Model, "scenario", _model_scenario)
205274
setattr(_arco.Model, "run_scenario", _model_run_scenario)
275+
setattr(_arco.SolveResult, "to_pandas", _solve_result_to_pandas)
276+
setattr(_arco.SolveResult, "to_polars", _solve_result_to_polars)
206277

207278

208279
if "block" not in __all__:

bindings/python/arco/arco.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,8 @@ class SolveResult:
407407
def is_unbounded(self) -> bool: ...
408408
def status_string(self) -> str: ...
409409
def solve_time_seconds(self) -> float: ...
410+
def to_pandas(self, *, table: str = "variables") -> object: ...
411+
def to_polars(self, *, table: str = "variables") -> object: ...
410412

411413
class VariableView:
412414
@property

bindings/python/tests/test_arco_stub_operators.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,15 @@ def test_model_stub_exposes_scenario_declaration_signatures() -> None:
119119
_assert_signatures_present(
120120
block=model_block, expected_signatures=expected_signatures
121121
)
122+
123+
def test_solve_result_stub_exposes_dataframe_export_signatures() -> None:
124+
source = (Path(__file__).resolve().parents[1] / "arco" / "arco.pyi").read_text()
125+
solve_result_block = _class_block(source=source, class_name="SolveResult")
126+
expected_signatures = [
127+
'def to_pandas(self, *, table: str = "variables") -> object: ...',
128+
'def to_polars(self, *, table: str = "variables") -> object: ...',
129+
]
130+
_assert_signatures_present(
131+
block=solve_result_block,
132+
expected_signatures=expected_signatures,
133+
)

0 commit comments

Comments
 (0)