Skip to main content
The outerproduct.model module contains the objects you work with after training. A plain Model handles predictions; a ReasoningModel extends it with feature-level explanations, counterfactual scenarios, and global driver analysis. Both are produced asynchronously: you call .wait() on a job handle and receive the model object once training completes.

Model

Model is produced by Trainer.configure().run().wait(). Use it when you need predictions but do not require explanations.

model.predict()

model.predict(dataset: Dataset) -> numpy.ndarray
Scores every row in dataset and returns a one-dimensional NumPy array of predictions. The SDK validates dataset’s column schema against the model’s training-time schema before the request leaves your machine, so mismatched or missing features raise a descriptive local error rather than a server-side 400.
dataset
Dataset
required
An op.Dataset. Build one with op.LocalDataset.from_pandas(df).upload(), op.LocalDataset.from_csv(...).upload(), or op.LocalDataset.from_numpy(...).upload(), or reference a connector. Column names must match the features seen during training.
returns
numpy.ndarray
A 1-D array of shape (n_samples,) containing the model’s prediction for each row. For binary classification, values are class-1 probabilities. For regression, values are the predicted target.
import outerproduct as op

op.init(api_key="your-api-key")

test_dataset = op.LocalDataset.from_pandas(X_new).upload()
predictions = model.predict(test_dataset)  # numpy.ndarray, shape (n_samples,)

print(predictions[:5])
# [0.82, 0.14, 0.67, 0.91, 0.33]

ReasoningModel

ReasoningModel is produced by op.reasoning.fit().wait(). It is a superset of Model (all predict() behaviour is identical) and adds explanation, counterfactual, and global-driver methods.
You need a ReasoningModel any time you want feature attributions or counterfactuals. Train one with op.reasoning.fit() instead of Trainer.

model.predict()

Identical to Model.predict() above: accepts a Dataset and returns a numpy.ndarray of shape (n_samples,).
import outerproduct as op

op.init(api_key="your-api-key")

model = op.reasoning.fit(
    dataset, task=op.Binclass(label_column="churn")
).wait()

test_dataset = op.LocalDataset.from_pandas(X_new).upload()
predictions = model.predict(test_dataset)

model.explain()

model.explain(dataset: Dataset) -> Reasoning
Returns per-sample feature attributions for every row in dataset. Each attribution value reflects how much that feature pushed the prediction up or down relative to the model’s baseline.
dataset
Dataset
required
Inference rows wrapped in an op.Dataset. Column schema must match the training data.
returns
Reasoning
A Reasoning object with three attributes.
test_dataset = op.LocalDataset.from_pandas(X_new).upload()
reasoning = model.explain(test_dataset)

print(reasoning.feature_names)
# ["age", "income", "credit_score"]

print(reasoning.attributions.shape)
# (100, 3)

if reasoning.rules:
    for rule in reasoning.rules:
        print(rule)
# "IF credit_score < 650 AND income < 40000 THEN churn (confidence 0.87)"

model.predict_and_explain()

model.predict_and_explain(dataset: Dataset) -> tuple[numpy.ndarray, Reasoning]
Returns predictions and local explanations in a single API round-trip. Prefer this over calling predict() and explain() separately when you need both.
dataset
Dataset
required
Inference rows wrapped in an op.Dataset.
returns
tuple[numpy.ndarray, Reasoning]
A two-element tuple. The first element is the predictions array (shape (n_samples,)). The second is a Reasoning object identical in structure to the one returned by explain().
test_dataset = op.LocalDataset.from_pandas(X_new).upload()
predictions, reasoning = model.predict_and_explain(test_dataset)

for i, (pred, attrs) in enumerate(zip(predictions, reasoning.attributions)):
    top_feature = reasoning.feature_names[attrs.argmax()]
    print(f"Row {i}: score={pred:.2f}, top driver={top_feature}")

model.get_global_drivers()

model.get_global_drivers() -> Reasoning
Returns a model-wide Reasoning summarising which features are most important across the entire training distribution, rather than for a specific set of input rows.
returns
Reasoning
A Reasoning object where attributions has shape (n_features,), one scalar importance value per feature. feature_names is aligned with attributions. rules is None for global results.
reasoning = model.get_global_drivers()

for name, importance in zip(reasoning.feature_names, reasoning.attributions):
    print(f"{name}: {importance:.4f}")

# credit_score: 0.3821
# income:       0.2945
# age:          0.1703
Use get_global_drivers() as a quick sanity check after training: if a feature you expected to matter is near zero, verify the column was present in the training data.

model.scenario()

model.scenario(
    dataset: Dataset,
    target_class: int = 1,
    n_trials: int | None = None,
    max_steps: int | None = None,
) -> ScenarioResult
Runs a counterfactual search for each row in dataset, finding the minimal feature edits that would shift the prediction to target_class. Each row is treated independently; results are returned together in a ScenarioResult.
dataset
Dataset
required
Query rows to analyse. Each row receives its own set of counterfactual candidates.
target_class
int
default:"1"
The class label you want the counterfactual to reach. Defaults to 1 (the positive class in binary classification).
n_trials
int | None
Number of independent search attempts per query row. More trials improve the chance of finding a shorter path but increase latency. Leave as None to use the OuterProduct default.
max_steps
int | None
Maximum number of feature edits allowed per attempt. Constraining this produces more actionable (smaller) counterfactuals. Leave as None to use the OuterProduct default.
returns
ScenarioResult
A ScenarioResult containing one QueryResult per input row. See ScenarioResult below for the full object tree.
import outerproduct as op
import pandas as pd

op.init(api_key="your-api-key")

queries = op.LocalDataset.from_pandas(pd.DataFrame([
    {"age": 25, "income": 50000, "credit_score": 620},
    {"age": 40, "income": 85000, "credit_score": 710},
])).upload()

result = model.scenario(queries, target_class=1)

for q in result.queries:
    print(f"Query {q.query_index}: baseline={q.baseline_prediction:.2f}")
    if q.already_at_desired_class:
        print("  Already at target class, no change needed.")
        continue
    for cf in q.counterfactuals:
        print(f"  Candidate ({cf.n_changes} changes):")
        for feature, change in cf.changes.items():
            print(f"    {feature}: {change.before}{change.after}")

ScenarioResult

ScenarioResult is returned by model.scenario(). It groups all counterfactual results for a batch of query rows.
queries
list[QueryResult]
One QueryResult per row in the input dataset, in the same order as the input rows.

QueryResult

A single row’s counterfactual analysis.
query_index
int
Zero-based index of this row within the input dataset, matching the original row order.
baseline_prediction
float
The model’s prediction for this row before any edits are applied.
already_at_desired_class
bool
True if the baseline prediction already belongs to target_class, meaning no change is required. When True, counterfactuals will be empty.
counterfactuals
list[Counterfactual]
Candidate counterfactual edits that would shift this row’s prediction to target_class, ordered by fewest changes first. May be empty if the search found no valid path within the configured max_steps and n_trials.

Counterfactual

One candidate set of edits for a single query row.
n_changes
int
Number of features modified in this counterfactual. Smaller is generally more actionable.
changes
dict[str, Change]
Mapping from feature name to a Change object describing what value it held before and what it would need to become.
for cf in query_result.counterfactuals:
    print(f"  {cf.n_changes} change(s):")
    for feature, change in cf.changes.items():
        print(f"    {feature}: {change.before}{change.after}")
# 2 change(s):
#   credit_score: 620 → 680
#   income: 50000 → 62000

Change

Describes a single feature edit within a Counterfactual.
before
any
The feature’s value in the original query row.
after
any
The value the feature would need to take for the counterfactual to reach target_class.

Predictor

Predictor wraps an arbitrary HTTP scoring endpoint so you can use an existing model as a teacher in OuterProduct’s distillation flow. Pass a Predictor as the teacher argument to op.reasoning.fit() to distil a ReasoningModel that mimics the teacher’s predictions while adding full reasoning.
op.model.Predictor(url: str, headers: dict | None = None)
url
string
required
The HTTP endpoint that accepts inference requests and returns predictions. OuterProduct will POST batches of rows to this URL during training.
headers
dict | None
Optional dictionary of HTTP request headers, such as {"Authorization": "Bearer <token>"}. Use this to authenticate against a secured scoring endpoint.
import outerproduct as op

op.init(api_key="your-api-key")

teacher = op.model.Predictor(
    url="https://my-scoring-service.example.com/predict",
    headers={"Authorization": "Bearer my-token"},
)

# Distil a ReasoningModel that learns from the teacher's predictions.
model = op.reasoning.fit(
    dataset,
    teacher=teacher,   # task can be omitted when a teacher is provided
).wait()

# The distilled model now supports explain(), get_global_drivers(), and scenario().
reasoning = model.explain(op.LocalDataset.from_pandas(X_new).upload())
When a teacher is provided, task is optional: OuterProduct queries the teacher to generate training labels. You can still supply a task with a label_column if you want to blend ground-truth labels with teacher predictions.