certifi-learn 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. certifi_learn-0.2.0/.github/workflows/wheels.yml +81 -0
  2. certifi_learn-0.2.0/.gitignore +42 -0
  3. certifi_learn-0.2.0/Cargo.lock +228 -0
  4. certifi_learn-0.2.0/Cargo.toml +18 -0
  5. certifi_learn-0.2.0/LICENSE +21 -0
  6. certifi_learn-0.2.0/PKG-INFO +216 -0
  7. certifi_learn-0.2.0/README.md +185 -0
  8. certifi_learn-0.2.0/docs/subgroup_pruning_proof.tex +466 -0
  9. certifi_learn-0.2.0/examples/detect_and_train.py +53 -0
  10. certifi_learn-0.2.0/examples/folktables_loader.py +77 -0
  11. certifi_learn-0.2.0/pyproject.toml +34 -0
  12. certifi_learn-0.2.0/python/certifi_learn/__init__.py +23 -0
  13. certifi_learn-0.2.0/python/certifi_learn/_dnf.py +333 -0
  14. certifi_learn-0.2.0/python/certifi_learn/_exact_backend.py +206 -0
  15. certifi_learn-0.2.0/python/certifi_learn/_gurobi.py +10 -0
  16. certifi_learn-0.2.0/python/certifi_learn/_linear.py +356 -0
  17. certifi_learn-0.2.0/python/certifi_learn/_mio.py +410 -0
  18. certifi_learn-0.2.0/python/certifi_learn/classifiers.py +228 -0
  19. certifi_learn-0.2.0/python/certifi_learn/detection.py +190 -0
  20. certifi_learn-0.2.0/python/certifi_learn/encoding/__init__.py +57 -0
  21. certifi_learn-0.2.0/python/certifi_learn/encoding/binarizer.py +267 -0
  22. certifi_learn-0.2.0/python/certifi_learn/encoding/data_handler/DataHandler.py +372 -0
  23. certifi_learn-0.2.0/python/certifi_learn/encoding/data_handler/__init__.py +2 -0
  24. certifi_learn-0.2.0/python/certifi_learn/encoding/data_handler/features/Binary.py +112 -0
  25. certifi_learn-0.2.0/python/certifi_learn/encoding/data_handler/features/Categorical.py +163 -0
  26. certifi_learn-0.2.0/python/certifi_learn/encoding/data_handler/features/Contiguous.py +107 -0
  27. certifi_learn-0.2.0/python/certifi_learn/encoding/data_handler/features/Feature.py +105 -0
  28. certifi_learn-0.2.0/python/certifi_learn/encoding/data_handler/features/Mixed.py +147 -0
  29. certifi_learn-0.2.0/python/certifi_learn/encoding/data_handler/features/__init__.py +7 -0
  30. certifi_learn-0.2.0/python/certifi_learn/encoding/data_handler/features/utils.py +68 -0
  31. certifi_learn-0.2.0/python/certifi_learn/encoding/data_handler/types.py +20 -0
  32. certifi_learn-0.2.0/python/certifi_learn/exact/__init__.py +11 -0
  33. certifi_learn-0.2.0/python/certifi_learn/exact/_api.py +347 -0
  34. certifi_learn-0.2.0/python/certifi_learn/exact/_common.py +323 -0
  35. certifi_learn-0.2.0/python/certifi_learn/measures.py +56 -0
  36. certifi_learn-0.2.0/src/lib.rs +524 -0
  37. certifi_learn-0.2.0/tests/test_classifiers.py +66 -0
  38. certifi_learn-0.2.0/tests/test_detection.py +95 -0
  39. certifi_learn-0.2.0/tests/test_exact_api.py +474 -0
  40. certifi_learn-0.2.0/tests/test_exact_correctness.py +341 -0
@@ -0,0 +1,81 @@
1
+ # Builds wheels (one abi3 wheel per platform covers Python >= 3.9), runs the tests,
2
+ # and publishes to PyPI when a version tag (v*) is pushed.
3
+ name: Wheels
4
+
5
+ on:
6
+ push:
7
+ branches: [main]
8
+ tags: ["v*"]
9
+ pull_request:
10
+ workflow_dispatch:
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ jobs:
16
+ build:
17
+ name: ${{ matrix.os }} ${{ matrix.target }}
18
+ runs-on: ${{ matrix.os }}
19
+ strategy:
20
+ fail-fast: false
21
+ matrix:
22
+ include:
23
+ - { os: ubuntu-latest, target: x86_64, test: true }
24
+ - { os: ubuntu-latest, target: aarch64, test: false }
25
+ - { os: macos-15-intel, target: x86_64, test: true }
26
+ - { os: macos-latest, target: aarch64, test: true }
27
+ - { os: windows-latest, target: x64, test: true }
28
+ steps:
29
+ - uses: actions/checkout@v6
30
+ - uses: actions/setup-python@v6
31
+ with:
32
+ python-version: "3.12"
33
+ - name: Build wheel
34
+ uses: PyO3/maturin-action@v1
35
+ with:
36
+ target: ${{ matrix.target }}
37
+ args: --release --out dist
38
+ manylinux: auto
39
+ - name: Test wheel
40
+ if: ${{ matrix.test }}
41
+ shell: bash
42
+ run: |
43
+ python -m pip install --upgrade pip
44
+ python -m pip install "$(ls dist/*.whl)[highs,test]"
45
+ python -m pytest tests -q
46
+ - uses: actions/upload-artifact@v6
47
+ with:
48
+ name: wheels-${{ matrix.os }}-${{ matrix.target }}
49
+ path: dist
50
+
51
+ sdist:
52
+ runs-on: ubuntu-latest
53
+ steps:
54
+ - uses: actions/checkout@v6
55
+ - name: Build sdist
56
+ uses: PyO3/maturin-action@v1
57
+ with:
58
+ command: sdist
59
+ args: --out dist
60
+ - uses: actions/upload-artifact@v6
61
+ with:
62
+ name: sdist
63
+ path: dist
64
+
65
+ publish:
66
+ name: Publish to PyPI
67
+ if: startsWith(github.ref, 'refs/tags/v')
68
+ needs: [build, sdist]
69
+ runs-on: ubuntu-latest
70
+ environment:
71
+ name: pypi
72
+ url: https://pypi.org/p/certifi-learn
73
+ permissions:
74
+ id-token: write # trusted publishing, no API token needed
75
+ steps:
76
+ - uses: actions/download-artifact@v7
77
+ with:
78
+ pattern: "*"
79
+ merge-multiple: true
80
+ path: dist
81
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,42 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .pytest_cache/
6
+ .ipynb_checkpoints/
7
+ .venv/
8
+
9
+ # Rust / maturin build
10
+ /target
11
+ /dist
12
+ *.so
13
+ *.pyd
14
+
15
+ # Data downloaded by folktables
16
+ data/
17
+
18
+ # Local working material (paper experiments, results); see tag neurips2026
19
+ outputs/
20
+ multirun/
21
+ batch_out/
22
+ batch_from_cluster/
23
+ plots/
24
+ NeurIPS_rebuttal/
25
+ bias_analysis/
26
+ maxviol/
27
+ maxviol_package_1.zip
28
+ bias_detect_maxviol.py
29
+ experiment_eval_maxviol.py
30
+ bias_detect_cluster.py
31
+ msd.py
32
+ spsf_mio.py
33
+ NN_fair_classifier.py
34
+ NN_plot.py
35
+ eval_plot.py
36
+ train_plot.py
37
+ plot_gap.py
38
+ parse_eval.py
39
+ .claude/
40
+ paper.pdf
41
+ *.png
42
+ *.pdf
@@ -0,0 +1,228 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "autocfg"
7
+ version = "1.5.1"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
10
+
11
+ [[package]]
12
+ name = "certifi_learn"
13
+ version = "0.2.0"
14
+ dependencies = [
15
+ "numpy",
16
+ "pyo3",
17
+ ]
18
+
19
+ [[package]]
20
+ name = "heck"
21
+ version = "0.5.0"
22
+ source = "registry+https://github.com/rust-lang/crates.io-index"
23
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
24
+
25
+ [[package]]
26
+ name = "libc"
27
+ version = "0.2.191"
28
+ source = "registry+https://github.com/rust-lang/crates.io-index"
29
+ checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
30
+
31
+ [[package]]
32
+ name = "matrixmultiply"
33
+ version = "0.3.11"
34
+ source = "registry+https://github.com/rust-lang/crates.io-index"
35
+ checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7"
36
+ dependencies = [
37
+ "autocfg",
38
+ "rawpointer",
39
+ ]
40
+
41
+ [[package]]
42
+ name = "ndarray"
43
+ version = "0.17.2"
44
+ source = "registry+https://github.com/rust-lang/crates.io-index"
45
+ checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d"
46
+ dependencies = [
47
+ "matrixmultiply",
48
+ "num-complex",
49
+ "num-integer",
50
+ "num-traits",
51
+ "portable-atomic",
52
+ "portable-atomic-util",
53
+ "rawpointer",
54
+ ]
55
+
56
+ [[package]]
57
+ name = "num-complex"
58
+ version = "0.4.6"
59
+ source = "registry+https://github.com/rust-lang/crates.io-index"
60
+ checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
61
+ dependencies = [
62
+ "num-traits",
63
+ ]
64
+
65
+ [[package]]
66
+ name = "num-integer"
67
+ version = "0.1.47"
68
+ source = "registry+https://github.com/rust-lang/crates.io-index"
69
+ checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
70
+ dependencies = [
71
+ "num-traits",
72
+ ]
73
+
74
+ [[package]]
75
+ name = "num-traits"
76
+ version = "0.2.19"
77
+ source = "registry+https://github.com/rust-lang/crates.io-index"
78
+ checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
79
+ dependencies = [
80
+ "autocfg",
81
+ ]
82
+
83
+ [[package]]
84
+ name = "numpy"
85
+ version = "0.29.0"
86
+ source = "registry+https://github.com/rust-lang/crates.io-index"
87
+ checksum = "6a5b15d63a5ff39e378daed0e1340d3a5964703ea9712eb09a0dc66fade996f4"
88
+ dependencies = [
89
+ "libc",
90
+ "ndarray",
91
+ "num-complex",
92
+ "num-integer",
93
+ "num-traits",
94
+ "pyo3",
95
+ "pyo3-build-config",
96
+ "rustc-hash",
97
+ ]
98
+
99
+ [[package]]
100
+ name = "once_cell"
101
+ version = "1.21.4"
102
+ source = "registry+https://github.com/rust-lang/crates.io-index"
103
+ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
104
+
105
+ [[package]]
106
+ name = "portable-atomic"
107
+ version = "1.15.0"
108
+ source = "registry+https://github.com/rust-lang/crates.io-index"
109
+ checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
110
+
111
+ [[package]]
112
+ name = "portable-atomic-util"
113
+ version = "0.2.8"
114
+ source = "registry+https://github.com/rust-lang/crates.io-index"
115
+ checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715"
116
+ dependencies = [
117
+ "portable-atomic",
118
+ ]
119
+
120
+ [[package]]
121
+ name = "proc-macro2"
122
+ version = "1.0.107"
123
+ source = "registry+https://github.com/rust-lang/crates.io-index"
124
+ checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
125
+ dependencies = [
126
+ "unicode-ident",
127
+ ]
128
+
129
+ [[package]]
130
+ name = "pyo3"
131
+ version = "0.29.3"
132
+ source = "registry+https://github.com/rust-lang/crates.io-index"
133
+ checksum = "700d18fa267b73b9b521fd7e13580e2f446916f176cacee1ab63fcc8191f1655"
134
+ dependencies = [
135
+ "libc",
136
+ "once_cell",
137
+ "portable-atomic",
138
+ "pyo3-build-config",
139
+ "pyo3-ffi",
140
+ "pyo3-macros",
141
+ ]
142
+
143
+ [[package]]
144
+ name = "pyo3-build-config"
145
+ version = "0.29.3"
146
+ source = "registry+https://github.com/rust-lang/crates.io-index"
147
+ checksum = "7b3fc0c4d08f6bb10e71fe39dfb9e2f59c6eb6854e22ec8092f50c69a4499adb"
148
+ dependencies = [
149
+ "target-lexicon",
150
+ ]
151
+
152
+ [[package]]
153
+ name = "pyo3-ffi"
154
+ version = "0.29.3"
155
+ source = "registry+https://github.com/rust-lang/crates.io-index"
156
+ checksum = "dfc0b8e19df29aad7086cf977bb0c2a2f143e30567eb113e9cf72b62ca698330"
157
+ dependencies = [
158
+ "libc",
159
+ "pyo3-build-config",
160
+ ]
161
+
162
+ [[package]]
163
+ name = "pyo3-macros"
164
+ version = "0.29.3"
165
+ source = "registry+https://github.com/rust-lang/crates.io-index"
166
+ checksum = "6100e8a4b5eba53afaa5ed078364851a0b2499c553a44c31026b929049b49dc6"
167
+ dependencies = [
168
+ "proc-macro2",
169
+ "pyo3-macros-backend",
170
+ "quote",
171
+ "syn",
172
+ ]
173
+
174
+ [[package]]
175
+ name = "pyo3-macros-backend"
176
+ version = "0.29.3"
177
+ source = "registry+https://github.com/rust-lang/crates.io-index"
178
+ checksum = "6143877a16e82b5a727b7127ff4cd86858a24a28d745d72f43e6f227c7b1bdb3"
179
+ dependencies = [
180
+ "heck",
181
+ "proc-macro2",
182
+ "quote",
183
+ "syn",
184
+ ]
185
+
186
+ [[package]]
187
+ name = "quote"
188
+ version = "1.0.47"
189
+ source = "registry+https://github.com/rust-lang/crates.io-index"
190
+ checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
191
+ dependencies = [
192
+ "proc-macro2",
193
+ ]
194
+
195
+ [[package]]
196
+ name = "rawpointer"
197
+ version = "0.2.1"
198
+ source = "registry+https://github.com/rust-lang/crates.io-index"
199
+ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
200
+
201
+ [[package]]
202
+ name = "rustc-hash"
203
+ version = "2.1.3"
204
+ source = "registry+https://github.com/rust-lang/crates.io-index"
205
+ checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
206
+
207
+ [[package]]
208
+ name = "syn"
209
+ version = "2.0.119"
210
+ source = "registry+https://github.com/rust-lang/crates.io-index"
211
+ checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
212
+ dependencies = [
213
+ "proc-macro2",
214
+ "quote",
215
+ "unicode-ident",
216
+ ]
217
+
218
+ [[package]]
219
+ name = "target-lexicon"
220
+ version = "0.13.5"
221
+ source = "registry+https://github.com/rust-lang/crates.io-index"
222
+ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
223
+
224
+ [[package]]
225
+ name = "unicode-ident"
226
+ version = "1.0.27"
227
+ source = "registry+https://github.com/rust-lang/crates.io-index"
228
+ checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
@@ -0,0 +1,18 @@
1
+ [package]
2
+ name = "certifi_learn"
3
+ version = "0.2.0"
4
+ edition = "2021"
5
+ license = "MIT"
6
+ readme = "README.md"
7
+
8
+ [lib]
9
+ name = "_core"
10
+ crate-type = ["cdylib"]
11
+
12
+ [dependencies]
13
+ pyo3 = { version = "0.29", features = ["extension-module", "abi3-py39"] }
14
+ numpy = "0.29"
15
+
16
+ [profile.release]
17
+ opt-level = 3
18
+ lto = true
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jiří Němeček, Mark Kozdoba, Illia Kryvoviaz, Tomáš Pevný, Jakub Mareček
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,216 @@
1
+ Metadata-Version: 2.4
2
+ Name: certifi-learn
3
+ Version: 0.2.0
4
+ Classifier: Programming Language :: Python :: 3
5
+ Classifier: Programming Language :: Rust
6
+ Classifier: License :: OSI Approved :: MIT License
7
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
8
+ Requires-Dist: numpy>=1.21
9
+ Requires-Dist: pandas
10
+ Requires-Dist: scikit-learn
11
+ Requires-Dist: folktables ; extra == 'examples'
12
+ Requires-Dist: pyomo>=6.7 ; extra == 'gurobi'
13
+ Requires-Dist: gurobipy ; extra == 'gurobi'
14
+ Requires-Dist: pyomo>=6.7 ; extra == 'highs'
15
+ Requires-Dist: highspy ; extra == 'highs'
16
+ Requires-Dist: pyomo>=6.7 ; extra == 'mio'
17
+ Requires-Dist: pytest ; extra == 'test'
18
+ Provides-Extra: examples
19
+ Provides-Extra: gurobi
20
+ Provides-Extra: highs
21
+ Provides-Extra: mio
22
+ Provides-Extra: test
23
+ License-File: LICENSE
24
+ Summary: Certifiably Intersectionally Fair & Interpretable models: detection of the most biased subgroup and training of fair interpretable classifiers
25
+ Author: Jiří Němeček
26
+ License: MIT
27
+ Requires-Python: >=3.9
28
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
29
+ Project-URL: Repository, https://github.com/Epanemu/CertIFI
30
+
31
+ # CertIFI: Certifiably Intersectionally Fair & Interpretable models
32
+
33
+ `certifi-learn` finds the **most biased subgroup** in data or in a model's predictions,
34
+ and trains **interpretable classifiers** (linear models and rule sets) that are
35
+ **certifiably fair** on all intersectional subgroups, for example all combinations
36
+ of sex, race and age group.
37
+
38
+ > [!NOTE]
39
+ > This library extends the NeurIPS 2026 paper
40
+ > *Intersectional Fairness via Mixed-Integer Optimization* (see [Citation](#citation)).
41
+ > Notably, it adds an exact branch-and-bound subgroup search (implemented in Rust)
42
+ > that replaces the Mixed-Integer Optimization (MIO) search for conjunction
43
+ > subgroups by default. The code used for the experiments in the paper is available
44
+ > at the tag [`neurips2026`](https://github.com/Epanemu/CertIFI/tree/neurips2026).
45
+
46
+ ## Features
47
+
48
+ - **Bias detection.** Find the subgroup with the largest violation of
49
+ - Subgroup Discrepancy / MSD,
50
+ - Statistical Parity Subgroup Fairness (SPSF),
51
+ - False Positive Subgroup Fairness (FPSF).
52
+
53
+ Subgroups can be
54
+ - **conjunctions** of protected attribute values (e.g. `SEX = Female AND RAC1P = Black`). The exact search is fast, needs no solver, and its result is proven optimal.
55
+ - **linear threshold functions** of the protected attributes, found by MIO.
56
+ - **Fair training.** Linear classifiers and DNFs (rule sets), trained to optimality
57
+ under the constraint that *every* subgroup has bias at most γ. Fairness
58
+ constraints are generated lazily: only the subgroups that are actually violated
59
+ are added, so there's no need to enumerate the exponentially many subgroups.
60
+ - A scikit-learn-style API that works with pandas DataFrames. Categorical
61
+ features are encoded automatically.
62
+
63
+ ## Installation
64
+
65
+ ```bash
66
+ pip install "certifi-learn[gurobi]"
67
+ ```
68
+
69
+ Prebuilt wheels are available for Linux, macOS and Windows (Python 3.9+). To install
70
+ the latest version from GitHub instead, you need a [Rust toolchain](https://rustup.rs/):
71
+
72
+ ```bash
73
+ pip install "certifi-learn[gurobi] @ git+https://github.com/Epanemu/CertIFI"
74
+ ```
75
+
76
+ Optional dependencies (extras):
77
+
78
+ | Extra | Needed for |
79
+ |---|---|
80
+ | `gurobi` | Training classifiers (Gurobi lazy-constraint callbacks) and MIO detection. Requires a [Gurobi license](https://www.gurobi.com/academia/academic-program-and-licenses/), free for academic use. |
81
+ | `highs` | MIO detection with the open-source [HiGHS](https://highs.dev/) solver, an alternative to Gurobi |
82
+ | `mio` | Pyomo only, if you bring your own solver installation |
83
+ | `examples` | The folktables example |
84
+
85
+ Bias detection on conjunction subgroups needs no extras.
86
+
87
+ ## Quickstart
88
+
89
+ ### Finding the most biased subgroup
90
+
91
+ ```python
92
+ from certifi_learn import find_subgroup
93
+ from certifi_learn.encoding import TabularEncoder
94
+
95
+ # protected: DataFrame of protected attributes, y: binary labels, y_pred: model predictions
96
+ enc = TabularEncoder(protected) # binarizes into literals like "SEX = Female"
97
+ P = enc.binary(protected, negations=False)
98
+ names = enc.binary_names(negations=False)
99
+
100
+ # bias in the data: subgroup whose share differs most between positive and negative labels
101
+ sg = find_subgroup(P, y, measure="msd")
102
+ print(f"MSD = {sg.value:.3f}, proven optimal: {sg.optimal}")
103
+ print(sg.describe(names))
104
+ # e.g., on ACS Income (examples/detect_and_train.py):
105
+ # MSD = 0.194, proven optimal: True
106
+ # AGEP between [17, 24.2)
107
+
108
+ # certify that no subgroup has bias above a threshold
109
+ sg = find_subgroup(P, y, measure="msd", gamma=0.25) # None: no subgroup has MSD > 0.25
110
+
111
+ # bias of a classifier
112
+ sg = find_subgroup(P, y_pred, measure="fpsf", y_true=y)
113
+
114
+ # also allow negated literals, e.g. "NOT RAC1P = White"
115
+ sg = find_subgroup(P, y_pred, measure="spsf", negations=True)
116
+
117
+ # linear subgroups (MIO, requires a solver)
118
+ sg = find_subgroup(P, y_pred, measure="spsf", subgroups="linear", solver="highs", time_limit=300)
119
+ ```
120
+
121
+ `find_subgroup` returns a `DetectedSubgroup` with **the violation `value`** (of the
122
+ chosen measure, on the given data), the membership `mask`, `optimal` (whether the subgroup is proven to be the most biased),
123
+ and `describe()` / `contains(X_new)`. If you pass `gamma=...`, it returns a subgroup
124
+ only if one violates the threshold, and `None` otherwise. That `None` certifies
125
+ γ-fairness, unless a warning reports that the search was stopped by the time limit.
126
+
127
+ ### Training a fair classifier
128
+
129
+ ```python
130
+ from certifi_learn import FairLinearClassifier, FairDNFClassifier
131
+
132
+ clf = FairLinearClassifier(gamma=0.01, measure="fpsf", time_limit=600)
133
+ clf.fit(X, y, X_protected) # X may include the protected attributes
134
+ y_pred = clf.predict(X_test)
135
+
136
+ print(clf.n_cuts_) # number of subgroups constrained during training
137
+ print(clf.describe_cut_subgroups()) # which ones
138
+
139
+ rules = FairDNFClassifier(n_terms=3, gamma=0.01, measure="fpsf").fit(X, y, X_protected)
140
+ print(rules.describe())
141
+ ```
142
+
143
+ Common parameters:
144
+
145
+ | Parameter | Values |
146
+ |---|---|
147
+ | `measure` | `"fpsf"`, `"fpsf_by_sd"` (FPSF constraints, with violating subgroups found by maximizing Subgroup Discrepancy on negative samples), `"spsf"`, `"msd"`, or `None` (no fairness constraint) |
148
+ | `subgroups` | `"conjunction"` or `"linear"` |
149
+ | `subgroup_negations` | Also allow negated literals in conjunction subgroups (e.g. `RAC1P != White`), which constrains a larger set of subgroups. Default `False`. |
150
+ | `gamma` | Fairness threshold. Every subgroup must have bias ≤ γ on the training data. |
151
+ | `detection_backend` | `"exact"` (default) or `"mio"`, for finding violating conjunction subgroups. Linear subgroups always use MIO. |
152
+ | `time_limit` | Solver time limit in seconds. The best solution found so far is returned. |
153
+
154
+ Model-specific parameters:
155
+
156
+ | Model | Parameter | Values |
157
+ |---|---|---|
158
+ | `FairLinearClassifier` | `sparsity` | Weight of the penalty on the proportion of non-zero coefficients (default 0) |
159
+ | `FairDNFClassifier` | `n_terms` | Number of terms (rules) of the DNF (default 3) |
160
+
161
+ γ-fairness is guaranteed on the training data. On new data, the bias can be
162
+ somewhat higher. Audit your model on held-out data with `find_subgroup`.
163
+
164
+ ## Bias measures
165
+
166
+ For a classifier h and a subgroup S:
167
+
168
+ | Measure | Definition |
169
+ |---|---|
170
+ | SPSF | P(S) · \|P(h=1) − P(h=1 \| S)\| |
171
+ | FPSF | P(S, y=0) · \|P(h=1 \| y=0) − P(h=1 \| S, y=0)\| |
172
+ | SD / MSD | \|P(S \| y=1) − P(S \| y=0)\|, i.e. how differently S is represented among positive and negative samples. Maximized over conjunctions, this is the [Maximum Subgroup Discrepancy](https://github.com/Epanemu/MSD). |
173
+
174
+ `certifi_learn.measures` evaluates these for a given subgroup mask.
175
+ `certifi_learn.exact` exposes the Rust search directly (`msd`, `spsf`, `fpsf`), with
176
+ options such as sample weights, negated literals and grouped one-hot columns. See
177
+ [`docs/subgroup_pruning_proof.tex`](docs/subgroup_pruning_proof.tex) for the
178
+ algorithm and its proof of correctness.
179
+
180
+ ## Example
181
+
182
+ [`examples/detect_and_train.py`](examples/detect_and_train.py) runs end to end on
183
+ the US Census data (ACS Income, via [folktables](https://github.com/socialfoundations/folktables)).
184
+ It finds the most biased subgroup in the data, trains an unconstrained and a fair
185
+ linear model, and audits both on test data:
186
+
187
+ ```bash
188
+ pip install "certifi-learn[gurobi,examples]"
189
+ python examples/detect_and_train.py
190
+ ```
191
+
192
+ ## Development
193
+
194
+ ```bash
195
+ pip install -e ".[gurobi,highs,test,examples]" # builds the Rust extension
196
+ pytest
197
+ ```
198
+
199
+ Tests that need HiGHS or a Gurobi license are skipped if those are not available.
200
+
201
+ ## Citation
202
+
203
+ ```bibtex
204
+ @inproceedings{nemecek2026intersectional,
205
+ author = {N\v{e}me\v{c}ek, Ji\v{r}\'{\i} and Kozdoba, Mark and Kryvoviaz, Illia and Pevn\'{y}, Tom\'{a}\v{s} and Mare\v{c}ek, Jakub},
206
+ booktitle = {Advances in Neural Information Processing Systems},
207
+ title = {Intersectional Fairness via Mixed-Integer Optimization},
208
+ volume = {39, Main Conference},
209
+ year = {2026}
210
+ }
211
+ ```
212
+
213
+ ## License
214
+
215
+ [MIT](LICENSE)
216
+