Metadata-Version: 2.4
Name: pacex
Version: 0.1.2
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
Requires-Dist: pandas>=1.5
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 German credit dataset (1 000 rows, mixed features)
data = fetch_openml("credit-g", version=1, as_frame=True)
df = data.frame
y = (df["class"] == "good").astype(int)

num_cols = ["age", "duration", "credit_amount"]
cat_cols = ["checking_status", "credit_history", "purpose", "personal_status"]

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(random_state=42).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 — with constraints and immutability
x_f = X_te_sc[0]
x_cf, report = explainer.explain(
    x_f,
    y_desired=1,                           # flip to "good credit"
    n_candidates=1200,                     # more candidates → better quality
    max_changed_features=2,               # change at most 2 features
    immutable=["age", "personal_status"], # these must not change
    constraints={
        "duration":      (None, 36),       # loan term ≤ 36 months
        "credit_amount": (0,    None),     # amount must stay non-negative
    },
)

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

# 6. Decode back to human-readable values
print(explainer.decode(x_cf))
# {'age': 34.0, 'duration': 24.0, 'credit_amount': 2500.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
