Metadata-Version: 2.4
Name: pacex
Version: 0.1.1
Summary: PACE: Proposal Assembly for Counterfactual Explanations
Author-email: Leonidas Christodoulou <lchristodoulou@gmail.com>
License: Apache-2.0
Project-URL: Homepage, https://github.com/leonidaschristodoulou/pace
Project-URL: Issues, https://github.com/leonidaschristodoulou/pace/issues
Keywords: counterfactual,explainability,xai,tabular,machine-learning
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.24
Requires-Dist: scikit-learn>=1.1
Dynamic: license-file

# PACE

**P**roposal **A**ssembly for **C**ounterfactual **E**xplanations — a counterfactual explainer for tabular classifiers.

PACE generates counterfactual explanations — the smallest change to a data point that flips the model's prediction.  It works with any binary classifier that exposes a `predict_proba` method, handles mixed numeric/categorical features natively, and supports actionability constraints.

## Install

```bash
pip install pacex
```

## Quick start

```python
from sklearn.datasets import fetch_openml
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from pace import PACE, FeatureInfo
from pace.preprocessing import make_preprocessor, feature_info_from_preprocessor

# 1. Load the Adult income dataset
data = fetch_openml("adult", version=2, as_frame=True)
df = data.frame.rename(columns={"capital-gain": "capital_gain", "hours-per-week": "hours_per_week"})
y = (df["class"] == ">50K").astype(int)

num_cols = ["age", "hours_per_week", "capital_gain"]
cat_cols = ["workclass", "education", "occupation"]

X_train_df, X_test_df, y_train, y_test = train_test_split(
    df[num_cols + cat_cols], y, test_size=0.2, random_state=42
)

# 2. Preprocess
pre = make_preprocessor(num_cols, cat_cols)
X_tr_sc = pre.fit_transform(X_train_df)   # numpy array, model space
X_te_sc = pre.transform(X_test_df)
feature_info = feature_info_from_preprocessor(pre, num_cols, cat_cols)

# 3. Train your model (any sklearn-compatible classifier)
model = RandomForestClassifier().fit(X_tr_sc, y_train)

# 4. Fit PACE once per (dataset, model) pair
explainer = PACE(
    X_train=X_tr_sc,
    predict_proba=model.predict_proba,
    feature_info=feature_info,
    pre=pre,
    num_cols=num_cols,
    cat_cols=cat_cols,
)

# 5. Explain a single instance
x_f = X_te_sc[0]
x_cf, report = explainer.explain(x_f, y_desired=1)

print(report)
# {'status': 'ok', 'l0': 2, 'l2': 0.83, 'p_cf': 0.71, ...}

# 6. Decode back to human-readable values
print(explainer.decode(x_cf))
# {'age': 34.0, 'hours_per_week': 45.0, 'workclass': 'Private', ...}
```

## Constraints and immutability

```python
x_cf, report = explainer.explain(
    x_f,
    y_desired=1,
    n_candidates=1200,          # more candidates → better quality
    max_changed_features=3,     # change at most 3 features
    immutable=["age", "sex"],   # these must not change
    constraints={
        "hours_per_week": (None, 60),   # hours ≤ 60
        "capital_gain":   (0, None),    # capital_gain ≥ 0
    },
)
```

Constraints are specified in the **original feature space** (before scaling).

## Purely numeric data

If your data has no categorical columns, skip `feature_info`:

```python
explainer = PACE(X_train=X_tr_sc, predict_proba=model.predict_proba)
x_cf, report = explainer.explain(x_f, y_desired=1)
```


## License

Apache 2.0
