afisp_rs 0.1.1

Clean-room Rust + PyO3 implementation of the AFISP core algorithms
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
# AFISP-RS Documentation

Complete user and API reference for the consolidated [AFISP](https://github.com/unc-vba/AFISP) Rust/Python implementation.

---

## Table of contents

1. [Overview]#overview
2. [Installation]#installation
3. [Package layout]#package-layout
4. [Workflow]#workflow
5. [API: `afisp_rs`]#api-afisp_rs
6. [API: `afisp` compatibility layer]#api-afisp-compatibility-layer
7. [Loss and utility functions]#loss-and-utility-functions
8. [Algorithm details]#algorithm-details
9. [Fidelity vs original AFISP]#fidelity-vs-original-afisp
10. [Rust library usage]#rust-library-usage
11. [Development and testing]#development-and-testing
12. [Publishing]#publishing

---

## Overview

**AFISP** (Adversarial/Fairness-oriented Interpretable Subgroup Phenotyping) is a two-stage method for auditing machine learning models on structured data:

1. **Stability analysis** — find worst-performing data subsets of varying sizes, conditioned on subgroup-defining features.
2. **Subgroup phenotyping** — extract interpretable rules describing who is in those subsets.

This crate implements both stages in **Rust** (performance + determinism) with **PyO3** Python bindings and an optional sklearn-compatible **`afisp`** wrapper matching the original Python package API.

---

## Installation

### From PyPI (recommended)

```bash
pip install afisp-rs
```

| Extra | Install command | Includes |
|-------|-----------------|----------|
| *(core)* | `pip install afisp-rs` | `numpy`, Rust extension |
| `original` | `pip install "afisp-rs[original]"` | sklearn, interpret (EBM), pandas, statsmodels, matplotlib, tqdm |
| `demo` | `pip install "afisp-rs[demo]"` | `original` + PyTorch |
| `test` | `pip install "afisp-rs[test]"` | `original` + pytest |

**Requirements:** Python ≥ 3.9. Prebuilt wheels are available for common Linux, macOS, and Windows platforms.

### From source

```bash
git clone https://github.com/na399/afisp-rs.git
cd afisp-rs
pip install maturin numpy
maturin develop --release --features extension-module
```

### Rust only (crates.io)

```toml
[dependencies]
afisp_rs = { version = "0.1", default-features = false }
```

---

## Package layout

```
afisp-rs/
├── src/                 # Rust core (stability, phenotype, SIRUS, metrics)
├── python/
│   ├── afisp_rs/        # Ergonomic Python API (Rust-backed)
│   └── afisp/           # sklearn-compatible wrappers (original API shape)
├── tests/               # pytest suite
├── examples/demo.py     # End-to-end example
└── scripts/             # Stability equivalence verifier
```

| Import | Module | When to use |
|--------|--------|-------------|
| `from afisp_rs import …` | Rust-backed API | New code, explicit control, best performance |
| `from afisp import …` | Compatibility layer | Drop-in replacement for original `afisp` sklearn API |

---

## Workflow

```text
                    ┌─────────────────────────────────────┐
                    │  Trained model + test set           │
                    │  (y_true, y_score, per-sample loss) │
                    └─────────────────┬───────────────────┘
                                      │
                    ┌─────────────────▼───────────────────┐
                    │  Stage 1: WorstSubsetFinder         │
                    │  Cross-fit conditional loss μ̂(x)   │
                    │  → worst-case subsets per fraction α│
                    └─────────────────┬───────────────────┘
                                      │
                    ┌─────────────────▼───────────────────┐
                    │  Select subset (e.g. max effect size) │
                    │  → binary subset_labels               │
                    └─────────────────┬───────────────────┘
                                      │
                    ┌─────────────────▼───────────────────┐
                    │  Stage 2: SubgroupPhenotyper        │
                    │  SIRUS or conjunction rule mining   │
                    │  → interpretable phenotype rules    │
                    └─────────────────────────────────────┘
```

**Inputs you always need:**

| Input | Shape | Description |
|-------|-------|-------------|
| Subgroup features `X` | `(N, F)` | Metadata / covariates defining subgroups (not model features unless intentional) |
| Samplewise losses | `(N,)` | Per-sample loss on the test set (Brier, cross-entropy, surrogate AUROC, etc.) |
| `y_true`, `y_score` | `(N,)` | Labels and predictions (for phenotyping AUROC tests) |

---

## API: `afisp_rs`

### `WorstSubsetFinder`

Identifies worst-performing subsets at multiple size fractions `α`.

#### Constructor

```python
WorstSubsetFinder(
    subset_fractions=None,  # default: [0.1, 0.15, 0.2, 0.4, 0.6, 0.8, 1.0]
    eps=0.0,                # tie-breaking noise scale for discrete μ̂
    random_state=0,         # SplitMix64 seed for tie noise
)
```

#### Methods

| Method | Description |
|--------|-------------|
| `fit_from_mu_hat(losses, mu_hat, fold_ids=None)` | Core fit when conditional loss estimates are precomputed |
| `fit_with_estimator(X, losses, estimator, cv=5, shuffle=False)` | sklearn KFold cross-fit → `μ̂` → stability core |
| `confidence_intervals()` | Analytical 95% CIs per subset fraction `(N, 2)` |
| `subset_masks()` | Boolean masks `(n_fractions, N)` |
| `observed_fractions()` | Actual fraction selected per mask |
| `compute_effect_sizes()` | Cohen's *d* per worst subset vs full dataset |
| `find_max_effect_size()` | `(index, max_d)` |
| `select_largest_below_threshold(metric_values, threshold, smaller_is_worse=True)` | Pick largest subset below a performance threshold |
| `subset_auc_table(y_true, y_score, n_bootstrap=100, confidence=0.95)` | Bootstrap AUROC per subset |

#### Attributes after fit

| Attribute | Description |
|-----------|-------------|
| `r_hats_` | Worst-case average loss per subset fraction |
| `mu_hat_` | Conditional loss estimates used for fit |
| `fold_ids_` | Fold assignment per sample (if cross-fit) |

#### Example

```python
from sklearn.ensemble import RandomForestRegressor
from afisp_rs import WorstSubsetFinder, brier_loss

losses = brier_loss(y_true, y_score)
finder = WorstSubsetFinder(subset_fractions=[0.1, 0.2, 0.4, 1.0], eps=1e-8, random_state=7)
finder.fit_with_estimator(X, losses, RandomForestRegressor(random_state=7), cv=10)

print(finder.r_hats_)
idx, d = finder.find_max_effect_size()
worst_mask = finder.subset_masks()[idx]
```

---

### `SubgroupPhenotyper`

Extracts interpretable rules for samples in vs. out of a worst subset.

#### Constructor

```python
SubgroupPhenotyper(
    backend="sirus",              # "sirus" or "conjunction"
    max_depth=3,
    min_support=50,
    max_candidates=5000,
    include_negations=True,
    max_literal_prevalence=0.95,
    min_lift=1.0,
    effect_threshold=0.4,         # Cohen's d filter (use 0.3 for original AFISP parity)
    bootstrap_samples=100,
    random_state=0,
    # SIRUS-specific
    sirus_num_trees=400,
    sirus_p0=0.025,
    sirus_num_rule=None,
    sirus_mtry=None,
    sirus_min_samples_leaf=5,
    sirus_sample_fraction=1.0,
    sirus_replace=True,
    numeric_quantiles=(0.25, 0.5, 0.75),
)
```

#### Methods

| Method | Description |
|--------|-------------|
| `fit(X, subset_labels, losses, *, y_true, y_score, performance_threshold, feature_names=None)` | Auto-binarize features, mine rules, filter |
| `fit_binary(X_binary, …, literal_names=None)` | Pre-binarized `(N, F)` uint8 matrix |
| `selected_rules()` | List of phenotype rule strings |
| `summary_table()` | DataFrame (if pandas installed) or list of dicts with AUROC CIs, *p*-values, effect sizes |
| `fit_metadata()` | Backend parameters and counts |

#### Example

```python
from afisp_rs import SubgroupPhenotyper

subset_labels = worst_mask.astype(np.uint8)
phen = SubgroupPhenotyper(backend="sirus", sirus_num_trees=300, effect_threshold=0.3)
phen.fit(
    X,
    subset_labels,
    losses,
    y_true=y_true,
    y_score=y_score,
    performance_threshold=0.85,
)
print(phen.summary_table())
```

---

### `prepare_binary_feature_matrix`

Converts mixed-type features to a binary matrix for rule mining:

- Binary/boolean columns → kept as-is
- Numeric columns → threshold bins at quantiles (default 25th, 50th, 75th)
- Categorical columns → one-hot encoding (requires pandas for non-DataFrame categoricals)

```python
from afisp_rs import prepare_binary_feature_matrix

X_bin, names = prepare_binary_feature_matrix(df, numeric_quantiles=(0.25, 0.5, 0.75))
```

---

## API: `afisp` compatibility layer

Mirrors the original [AFISP Python package](https://github.com/unc-vba/AFISP) sklearn API.

```python
from afisp import WorstSubsetFinder, SubgroupPhenotyper
```

Requires `pip install "afisp-rs[original]"` for EBM defaults and plotting.

### `afisp.WorstSubsetFinder`

sklearn `BaseEstimator` wrapper.

| Parameter | Default | Notes |
|-----------|---------|-------|
| `subset_fractions` | `[0.1, 0.15, 0.2, 0.4, 0.6, 0.8, 1.0]` | |
| `conditional_loss_model` | `ExplainableBoostingRegressor` | Any sklearn regressor with `fit`/`predict` |
| `cv` | `10` | KFold splits, `shuffle=False` |
| `eps` | `0.0` | |
| `verbose` | `False` | |

| Method | Maps to |
|--------|---------|
| `fit(X, losses, feature_names=None)` | EBM/estimator cross-fit → `afisp_rs` |
| `confidence_intervals()` | `cis_` array |
| `subset_masks()` | List of 1D boolean masks |
| `check_subset_sizes(plot=True)` | Diagnostic plot vs requested fractions |
| `compute_effect_sizes(plot=False)` | Cohen's *d* per fraction |
| `find_max_effect_size()` | `(index, value)` |

Fit attributes: `R_hats_`, `mu_hat_`, `cis_`, `num_samples_`.

### `afisp.SubgroupPhenotyper`

| Parameter | Default | Notes |
|-----------|---------|-------|
| `method` | `"DecisionList"` | `"SIRUS"` or `"DecisionList"` |
| `depth` | `2` | Max rule depth |
| `p0` | `0.025` | SIRUS stability threshold |
| `rule_max` | `50` | Max SIRUS rules |
| `cv` | `False` | SIRUS p0 CV (falls back to Rust SIRUS) |

**Rust-backed phenotyping** requires keyword args on `fit()`:

```python
phen.fit(
    X, subset_labels, test_loss,
    method="SIRUS",
    y_true=y_true,
    y_score=y_score,
    performance_threshold=0.85,
)
```

| `method` | Backend |
|----------|---------|
| `"SIRUS"` | R `sirus` if installed, else Rust SIRUS |
| `"DecisionList"` | Rust conjunction miner |

`generate_subgroup_table(y_test, test_preds, loss_fn=...)` returns a pandas DataFrame with Phenotype, Performance, N, Lower, Upper columns.

---

## Loss and utility functions

### `afisp_rs` (Rust-backed)

```python
from afisp_rs import cross_entropy_loss, brier_loss, zero_one_loss, roc_auc_score
```

| Function | Inputs | Output |
|----------|--------|--------|
| `cross_entropy_loss(y_true, y_pred)` | uint8 labels, float probs | per-sample CE |
| `brier_loss(y_true, y_pred)` | uint8 labels, float probs | per-sample Brier |
| `zero_one_loss(y_true, y_pred_binary)` | uint8 labels, uint8 preds | per-sample 0/1 |
| `roc_auc_score(y_true, y_score)` | uint8 labels, float scores | scalar AUROC |

### `afisp.utils` (compatibility + surrogates)

```python
from afisp.utils import (
    cross_entropy, brier, zero_one_loss, mse,
    clip_predictions, logit, cohens_d, bootstrap_ci,
    roc_auc_surrogate, torch_roc_auc_surrogate,
)
```

PyTorch surrogates require `pip install "afisp-rs[demo]"`.

---

## Algorithm details

### Stage 1: Stability analysis

For each subset fraction `α` and each cross-validation fold:

1. Compute fold-local quantile threshold `η̂ = Q_{1-α}(μ̂)` on out-of-fold samples.
2. Apply tie noise: `μ̂ + U` where `U ~ Uniform(0, ε)` via deterministic SplitMix64.
3. Subset mask: `h = 1[μ̂ + U ≥ η̂]`.
4. Influence-function estimator:

   ```
   ψ = max(μ̂ + U - η̂, 0) / α + η̂ + h · (loss - μ̂) / α
   ```

5. Risk `R̂_α = mean(ψ)`; CI from normal approximation with `σ̂`.

### Stage 2: Rule mining

**SIRUS backend (`backend="sirus"`):**

1. Build randomized shallow binary tree forest predicting worst-subset membership.
2. Extract path rules from non-root nodes.
3. Keep rules with frequency ≥ `p0` across trees.
4. Score and filter (see below).

**Conjunction backend (`backend="conjunction"`):**

1. Enumerate depth-limited literal conjunctions with bitset pruning.
2. Rank by support and lift.
3. Score and filter.

**Statistical filtering (both backends):**

1. Bootstrap AUROC CI within each rule-defined subgroup.
2. One-sided *z*-test vs `performance_threshold`.
3. Holm–Bonferroni correction (α = 0.05).
4. Cohen's *d* > `effect_threshold`.

---

## Fidelity vs original AFISP

| Component | Match quality | Notes |
|-----------|---------------|-------|
| Stability ψ / CIs | High | Validated to `1e-10` vs independent NumPy reference |
| Fold-stratified quantiles | High | |
| Tie noise (SplitMix64) | High | |
| `subset_masks()` with `eps > 0` | Differs | Original omits tie noise in masks; this crate includes it (consistent with ψ) |
| EBM cross-fit | Via `afisp` layer | Requires `interpret` |
| R SIRUS | Optional | Rust SIRUS default |
| SkopeRules DecisionList | Approximate | Conjunction miner substitute |
| SIRUS `cv=True` for p0 | Not implemented | |

See [DESIGN.md](DESIGN.md) for engineering rationale.

---

## Rust library usage

Public types (no Python required):

```rust
use afisp_rs::{WorstSubsetFinderCore, SubgroupPhenotyperCore};

let mut finder = WorstSubsetFinderCore::new(
    vec![0.1, 0.2, 0.4, 1.0],
    1e-8,
    7,
);
finder.fit_from_mu_hat(losses, mu_hat, Some(fold_ids))?;
let masks = &finder.masks;
let risks = &finder.r_hats;
```

Build with Python extension:

```bash
cargo build --features extension-module
```

---

## Development and testing

```bash
# Rust unit tests (no Python)
cargo test --no-default-features

# Full Python suite
maturin develop --release --features extension-module
pytest -q

# Stability equivalence report
python scripts/verify_stability.py
```

### Test coverage

| Test file | Purpose |
|-----------|---------|
| `tests/test_smoke.py` | End-to-end SIRUS + conjunction |
| `tests/test_stability_reference.py` | Native vs NumPy reference |
| `tests/test_original_parity.py` | Original ψ formula parity |

---

## Publishing

Maintainers: see [PUBLISHING.md](PUBLISHING.md).

| Registry | Package name | Trigger |
|----------|--------------|---------|
| PyPI | `afisp-rs` | Push tag `v*` |
| crates.io | `afisp_rs` | Push tag `v*` |

---

## References

- Original AFISP repository: https://github.com/unc-vba/AFISP
- PyPI: https://pypi.org/project/afisp-rs/
- crates.io: https://crates.io/crates/afisp_rs
- docs.rs: https://docs.rs/afisp_rs

---

## License

MIT License. See [LICENSE](LICENSE).