Source code for fair_seldonian.config
from __future__ import annotations
from dataclasses import dataclass
from .constraints.expression_tree import validate_constraint
from .constraints.inequalities import Inequality
#: The names this module contributes to the public API. autodoc documents
#: exactly these, so the API reference stays the surface users are meant to
#: call rather than every helper that happens to lack a leading underscore.
__all__ = [
"DEFAULT_CONFIG",
"SeldonianConfig",
]
[docs]
@dataclass(frozen=True)
class SeldonianConfig:
"""Configuration for the Seldonian algorithm.
The ``constraint`` is the fairness/behavioral requirement that
:func:`~fair_seldonian.algorithms.qsa.QSA` must certify, given as a
reverse-Polish (postfix) string over the per-group confusion-matrix cells
``TP(g)``, ``FP(g)``, ``FN(g)``, ``TN(g)``. You can supply it two ways:
* a **built-in fairness constraint** from
:mod:`fair_seldonian.constraints.fairness` (recommended) -
:func:`~fair_seldonian.constraints.fairness.demographic_parity`,
:func:`~fair_seldonian.constraints.fairness.equal_opportunity`,
:func:`~fair_seldonian.constraints.fairness.equalized_odds`,
:func:`~fair_seldonian.constraints.fairness.error_rate`, or
:func:`~fair_seldonian.constraints.fairness.error_rate_parity`; or
* a **custom postfix string** you write yourself.
Both produce the same kind of string, so they are interchangeable::
from fair_seldonian import SeldonianConfig, demographic_parity
SeldonianConfig(constraint=demographic_parity(epsilon=0.1))
SeldonianConfig(constraint="PR(1) PR(0) - abs 0.1 -")
The constraint is validated on construction (via
:func:`~fair_seldonian.constraints.expression_tree.validate_constraint`), so a
malformed custom string raises ``ValueError`` immediately rather than failing
inside QSA.
:param delta: the constraint must hold with probability >= 1 - delta.
:param inequality: concentration inequality used for the confidence bound.
:param constraint: the postfix constraint string (see above).
:param candidate_ratio: fraction of data used to pick the candidate solution.
:param optimizer: any ``method`` accepted by :func:`scipy.optimize.minimize`.
:param max_iter: iteration cap handed to the optimizer.
:param penalty: weight on the constraint violation in candidate selection.
Candidate selection minimises ``log_loss + penalty * max(0, u)`` where ``u``
is the predicted upper bound on the constraint.
A continuous penalty matters more than it looks. The tempting alternative
is a hard barrier - return some large constant plus ``u`` when infeasible
and the loss when feasible - but SciPy's Powell convergence test is
*relative*: with an objective of order 1e4 and the default ``ftol=1e-4``
the stopping threshold is about 1.0, while ``u`` varies by only about 1e-2
over the whole parameter space. Powell then reports success after a single
iteration while still infeasible, and ``max_iter`` never binds. An exact
penalty keeps the objective order 1 and gives the optimizer usable signal
on the infeasible side.
"""
delta: float = 0.05
inequality: Inequality = Inequality.HOEFFDING_INEQUALITY
constraint: str = "TP(1) TP(0) - abs 0.25 TP(1) * -"
candidate_ratio: float = 0.40
optimizer: str = "Powell"
max_iter: int = 10000
penalty: float = 100.0
def __post_init__(self) -> None:
if not 0 < self.delta < 1:
raise ValueError(f"delta must be in (0, 1), got {self.delta}")
if not 0 < self.candidate_ratio < 1:
raise ValueError(
f"candidate_ratio must be in (0, 1), got {self.candidate_ratio}"
)
if self.penalty <= 0:
raise ValueError(f"penalty must be positive, got {self.penalty}")
validate_constraint(self.constraint)
#: The configuration every entry point falls back to when none is passed --
#: :func:`~fair_seldonian.algorithms.qsa.QSA`, :func:`~fair_seldonian.models.ghat`
#: and the rest all default to it. Being a frozen dataclass it is safe to share;
#: build a variant with ``dataclasses.replace`` or by constructing a new
#: :class:`SeldonianConfig`.
DEFAULT_CONFIG = SeldonianConfig()