Skip to main content
Version: v1.4.1

Optimization

v2 (XGM) deployments don't just predict — they prescribe. Given a row, a budget, and a cost per change, the optimizer searches the model's own contribution curves for the feature changes that best move the outcome. The solution is exact (a global optimum over the candidate grid, not a heuristic) and personalized: because contribution curves are conditioned on each row's immutable values, two similar rows can receive genuinely different prescriptions.

Four endpoints cover the common shapes of the question:

EndpointQuestion it answers
/optimize"Best changes for this row within a budget?"
/optimize/batch"Best changes for each of these rows?"
/counterfactual"Cheapest way to get this row to a target outcome?"
/portfolio"How do I split one shared budget across these rows?"

All four are v2-only — calling them on a v1 deployment returns 400 Bad Request. Rows are sent in the model's fitted feature space (original units); deployment preprocessing is not applied.

See it work: pick a customer, drag the budget, and watch the solver's cost-vs-outcome frontier. Note how the same budget buys a very different outcome per customer — that's the personalization:

Live optimization frontierreal optimizer output
baseline 26.0%$0$200 spend26%2%
Prescription at $100
discount_applied00.1
avg_order_value150174.0514
churn risk4.0%reduction22.0ppspend used$98
Frontiers precomputed by the real solver (/v1/optimize, cost structure: discount $500/unit, AOV $2/unit). Compare customers — the same budget buys a very different outcome for each row. That is personalization from interaction terms, not a global rule.

The result envelope

Solver results always come back as HTTP 200 with a status envelope — branch on status, not on the HTTP code:

{ "status": "success", "model_name": "model", "results": { ... } }
{
"status": "error",
"error": {
"code": "invalid_parameter",
"message": "Joint candidate grid too large for coupled group ...",
"context": {
"remedies": [
"lock one of the coupled features",
"lower n_grid",
"raise max_joint_candidates"
]
}
}
}

error.code is stable and machine-readable; error.context carries structured detail, including remedies where applicable.

Common parameters

These are accepted by all four endpoints unless noted:

mutable_featureslist[str]
Features the optimizer may change. Defaults to every optimizable feature minus any features the model was configured to hold immutable.
cost_structuredict
Cost per feature change, in ORIGINAL units (e.g. {"discount_applied": 120}). Omitted features cost nothing. Falls back to the model's configured costs.
feature_boundsdict
Per-feature bounds in original units — [min, max] for numeric, a list of allowed categories otherwise. May only TIGHTEN the model's configured ranges.
n_gridintdefault: 256
Candidate grid density for numeric features.
cost_resolutionintdefault: 200
Discretization of the cost axis.
max_joint_candidatesint
Cap on the joint grid size for coupled feature groups. Exceeding it returns an invalid_parameter error with remedies.
Feasibility rules are enforced server-side

Feasibility rules configured on the model (e.g. "these two values can never co-occur") are hard constraints — they backfill from the model's persisted optimization config and cannot be overridden per request.


/optimize

POST/v1/optimize

Budget-constrained optimization for a single row.

Request

rowdictRequired
Feature values for the row, in original units. Must cover every fitted feature; extra keys are ignored.
budgetfloatRequired
Hard intervention-cost budget (>= 0).

Plus any common parameters.

{
"row": { "tenure_days": 420, "avg_order_value": 86.5, "discount_applied": 0.0, "plan": "premium" },
"budget": 150,
"mutable_features": ["discount_applied", "avg_order_value"],
"cost_structure": { "discount_applied": 500, "avg_order_value": 2 }
}

Response (results)

FieldDescription
optimal_featuresThe full optimized row, original units
predictionModel outcome at the optimized row (response scale)
total_costTrue cost of the proposed changes
frontierThe whole cost-vs-outcome frontier from 0 to budget, as [cost, prediction] pairs
frontier_solutionsFull feature dicts aligned index-wise with frontier
global_optimumtrue — the solution is exact, not heuristic
infeasible_baselinetrue when the input row already violates a feasibility rule

The frontier is produced for free by the solver — one call gives you the optimal action at every budget level up to the one you set, which is useful for "how much budget is actually worth spending?" analyses.


/optimize/batch

POST/v1/optimize/batch

Optimize many rows in one call — the model is built once and every row is solved against it.

Request

rowslist[dict]Required
Rows to optimize, in original units.
objectivestrdefault: 'optimum'
'optimum' (best achievable outcome per row), 'budget' (best outcome under a hard per-row cost budget), or 'pareto' (the cost-vs-outcome frontier, or the cheapest point reaching target when given).
budgetfloat
Per-row cost budget. Required when objective='budget'. Note: applies to EACH row — for one shared budget use /portfolio.
targetfloat
Outcome target for objective='pareto' frontier selection.
cost_weightfloatdefault: 0.0
Soft cost trade-off; applies only to objective='optimum'.
directionstr
'minimize' | 'maximize' — overrides the model's configured optimization direction for this call. Omit to use the model's default. Batch only.
per_row_immutablelist[list[str]]
Aligned to rows: features held at that specific row's current value.

Plus any common parameters.

{
"rows": [
{ "tenure_days": 420, "avg_order_value": 86.5, "discount_applied": 0.0 },
{ "tenure_days": 31, "avg_order_value": 22.0, "discount_applied": 0.1 }
],
"objective": "budget",
"budget": 100,
"direction": "maximize",
"mutable_features": ["discount_applied", "avg_order_value"]
}

Response

results is a list of per-row result dicts (aligned with rows), each carrying the optimized features, outcome, and cost for that row — objective-dependent fields (e.g. the frontier under pareto) included.

direction vs objective

direction and objective are orthogonal: objective selects the search strategy, direction sets the optimization sense (whether "better" means a higher or lower prediction).


/counterfactual

POST/v1/counterfactual

The minimal-cost set of changes that gets a row to a desired outcome.

Request

rowdictRequired
Feature values for the row, in original units.
desired_outcomefloatRequired
The outcome to reach (response scale, e.g. 0.8 for an 80% probability).

Plus any common parameters.

Response

FieldDescription
foundtrue if the target is reachable; false returns the best achievable point instead
desired_outcomeEcho of the requested target
original_predictionOutcome at the unmodified row
predictionOutcome at the counterfactual row
total_costCost of the changes
counterfactual_featuresThe full modified row
feature_changesJust the features that changed, with before/after values

/portfolio

POST/v1/portfolio

Allocate one shared budget across a set of rows, exactly and globally optimally. Unlike /optimize/batch with objective='budget' — where the budget applies to each row — here it is a single pool: the solver decides which rows are worth spending on at all.

Request

rowslist[dict]Required
Rows to allocate across, in original units.
total_budgetfloatRequired
ONE shared intervention-cost budget for the whole set.
valuelist[float]
Per-row non-negative weights aligned to rows (e.g. each customer's revenue). Given: the objective is value-weighted improvement. Omitted: unweighted total improvement.
per_row_immutablelist[list[str]]
Aligned to rows: features held at that row's current value.

Plus any common parameters.

Response (portfolio)

FieldDescription
allocationsPer-row allocation results, aligned to rows
total_costTotal spend across all rows
total_improvementTotal outcome improvement delivered
total_weighted_improvementValue-weighted improvement (when value given)
n_fundedHow many rows received any spend
objectiveWhich objective was solved

Batch optimization from the platform

You can also run batch optimizations against hosted datasets — without managing deploy keys or payloads — via the SDK's optimiser surface. It creates a reusable named policy and proxies to the deployed model's runtime:

result = client.workflow.optimise_model(
model_id=model_id,
objective="pareto",
dataset_id=dataset_id,
constraints={
"immutable": ["tenure_days"],
"bounds": {"discount_applied": [0, 0.3]},
},
direction="maximize", # optional; omit to use the model's default
)

Saved optimiser policies accept the same keys as /optimize/batch (objective, direction, budget, target, cost_weight, mutable_features, per_row_immutable, feature_bounds, cost_structure, max_joint_candidates, n_grid, cost_resolution), and per-run params override the saved policy. Results are stored on the run and retrievable via client.optimisers.get_optimiser_run().