Skip to content

effector is purely numpy based

effector speaks one language: numpy. Both things you hand it live in numpy and nowhere else:

  • data — a 2-D numeric numpy array, shape (N, D).
  • model (and the optional model_jac) — a numpy → numpy callable: X: (N, D) → (N,) for the model, → (N, D) for the Jacobian.
  • metadata — everything else (feature names, types, target name, scaling) travels in a single schema= argument.

That's the whole contract. If your model was trained in PyTorch, TensorFlow, or on a pandas DataFrame, you wrap it into a numpy → numpy function — and that wrapper is exactly where the framework-specific concerns (dtype, device, batching) belong, because only you know them.

Why numpy?

A numpy array is the one format PyTorch, TensorFlow, JAX, scikit-learn, XGBoost, and plain Python functions all speak. Standing on numpy makes effector fast (the model is called as-is, with zero per-call conversion) and lets it explain any model without shipping a single framework adapter.


The base case: numpy in, numpy model

Nothing to wrap — just call it.

import numpy as np
import effector

X = np.random.uniform(-1, 1, (500, 3))            # (N, D) numpy
model = lambda A: A[:, 0] + A[:, 1] * (A[:, 2] > 0)   # numpy -> numpy

effector.PDP(X, model).plot(feature=0)

Add a schema whenever you want readable axes:

schema = effector.Schema(feature_names=["age", "income", "region"])
effector.PDP(X, model, schema=schema).plot(feature=0)

PyTorch

Wrap the network in a numpy → numpy function. This is the entire integration — and the natural home for eval(), no_grad, device, and dtype:

import torch

net.eval()
device = next(net.parameters()).device

def model(X):                                  # numpy (N, D) -> numpy (N,)
    with torch.no_grad():
        t = torch.as_tensor(X, dtype=torch.float32, device=device)
        return net(t).cpu().numpy().ravel()

effector.PDP(X, model).plot(feature=0)

Bonus: exact gradients for RHALE / DerPDP

The Jacobian is also just numpy → numpy, so you can hand effector the exact autograd gradient instead of its numerical fallback:

def model_jac(X):                          # numpy (N, D) -> numpy (N, D)
    t = torch.as_tensor(X, dtype=torch.float32,
                        device=device).requires_grad_(True)
    net(t).sum().backward()
    return t.grad.cpu().numpy()

effector.RHALE(X, model, model_jac).plot(feature=0)

TensorFlow / Keras

Keras .predict already takes numpy in and returns numpy out, so the wrapper is almost a no-op:

model = lambda X: net.predict(X, verbose=0).ravel()
effector.PDP(X, model).plot(feature=0)

Starting from a pandas DataFrame

Most workflows begin with a DataFrame. Convert it once with effector.from_dataframe, which reads column names, dtypes, and category labels into (X, schema) — and never touches your model:

import dataclasses
import effector

X_np, schema = effector.from_dataframe(df)     # numpy matrix + a populated Schema
print(schema.feature_types)                    # inspect the guesses...
# Schema is a frozen dataclass — override anything you don't like with replace:
schema = dataclasses.replace(schema, feature_types=["ordinal", "continuous", "nominal"])

effector.PDP(X_np, model, schema=schema).plot(feature=0)

Inspect the returned schema

from_dataframe infers types from dtypes, but an integer column is ambiguous — ordinal, continuous, or a label-encoded nominal are all plausible. effector emits a UserWarning for those guesses; check them and set feature_types explicitly when needed.

A model that consumes a DataFrame (e.g. an sklearn Pipeline)

If your model needs a DataFrame (a ColumnTransformer/OneHotEncoder pipeline trained on string columns), reconstruct the frame inside your wrapper:

levels = ["clear", "mist", "rain"]

def model(X):                                  # numpy grid -> numpy predictions
    frame = pd.DataFrame({
        "hour":    X[:, 0],
        "temp":    X[:, 1],
        "weather": pd.Categorical.from_codes(
            np.clip(np.round(X[:, 2]), 0, 2).astype(int), levels),
    })
    return pipeline.predict(frame)

X_np, schema = effector.from_dataframe(df)
effector.PDP(X_np, model, schema=schema).plot(feature=0)

You own the reconstruction, so it's visible and testable — effector never guesses how to call your model.


What changed

Passing a DataFrame straight into a constructor no longer works — it raises, with the fix in the message:

effector.PDP(df, model)
# TypeError: effector is numpy-only: `data` must be a 2-D numeric numpy array,
#   not a pandas DataFrame. Convert it first:
#       X, schema = effector.from_dataframe(df)
#   and pass a numpy->numpy `model` ...

The one-line fix is X, schema = effector.from_dataframe(df).


See also: the API overview for the full constructor signature, and the design contract (R8 constructor contract, R10 input contract) for the authoritative spec.