API Reference

Complete reference documentation for developers building fairness-aware ML systems. All functions, classes, parameters, and return types.

vfairness is organized into eight modules following the ML fairness pipeline. Hover any card for details:

1
Data & Preprocessing
vfairness.preprocessing
Feature transforms,
bias auditing
Key APIs: BiasDetector, FeatureEngineeringAnalyzer, proxy variable detection, historical pattern analysis.
Purpose: Scans training data for biases and applies fairness-aware feature transformations before model training.
2
Training-Time Interventions
vfairness.in_processing
Losses, constraints,
regularizers
Key APIs: GroupFairnessLoss, ExponentiatedGradient, FairnessRegularizer, FairClassifier.
Purpose: Embeds fairness constraints directly into the training loop so the model learns to be fair and accurate simultaneously.
3
Prediction-Time Interventions
vfairness.post_processing
Calibration,
threshold tuning
Key APIs: GroupCalibrator, CalibrationAnalyzer (Platt, Isotonic, Beta, Temperature scaling), ThresholdOptimizer.
Purpose: Adjusts model outputs after training — calibrating scores per group and optimizing decision thresholds for fairness.
4
Evaluation & Measurement
vfairness.evaluation
Metrics, statistics,
visualization
Key APIs: FairnessAnalyzer, FairExplAIner, 30+ classification/regression/ranking metrics, bootstrap CI, permutation tests.
Purpose: Computes fairness metrics, runs statistical validation, performs intersectional analysis, and generates explanations.
5
Operations & Monitoring
vfairness.operations
CI/CD, MLOps,
monitoring
Key APIs: ModelFairnessGate, FairnessMonitor, FairnessDriftDetector, AdaptiveThresholdManager, TemporalFairnessAnalyzer.
Purpose: CI/CD deployment gates, production drift detection, adaptive alerting, and temporal fairness tracking.
6
Reporting & Dashboards
vfairness.operations.reporting
Interactive dashboards,
automated reports
Key APIs: MetricsStore, FairnessDashboard, ReportGenerator, InteractiveDashboard, multi-format export (HTML, PDF, JSON).
Purpose: Generates stakeholder-ready fairness reports, builds interactive dashboards, and applies privacy safeguards to metrics.
7
Experimentation
vfairness.operations.experimentation
A/B testing,
Pareto & causal
Key APIs: FairnessExperiment, FairnessPowerAnalyzer, ExperimentAnalysis, ParetoOptimizer, causal decomposition.
Purpose: Runs fairness-aware A/B tests with power analysis, finds Pareto-optimal trade-offs, and decomposes disparity into causal components.
8
Workflow Integration
vfairness.operations.cicd
CI/CD gates,
pytest & pre-commit
Key APIs: ModelFairnessGate, HierarchicalGateConfig, FairnessReportCard, pytest plugin, pre-commit hooks, MLflow/W&B logging.
Purpose: Embeds fairness checks into CI/CD pipelines, pytest suites, and pre-commit hooks so unfair models are caught before deployment.
9
LLM Fairness Testing
vfairness.llm
Counterfactual testing,
benchmarks & output analysis
Key APIs: LLMApiProxy, CounterfactualTester (9 strategies), OutputAnalyzer, BenchmarkRunner (BBQ/BOLD/HolisticBias), DecodingTrustRunner (8 dimensions), NonDeterminismAnalyzer, IntersectionalAnalyzer, CoTFaithfulnessAnalyzer.
Purpose: Test LLMs for bias without training data. Counterfactual prompt testing, non-determinism management (noise offset), and pluggable output analysis.
10
Agent Fairness Testing
vfairness.agents
Tool bias, RAG bias,
correspondence testing
Key APIs: CorrespondenceTester, ToolBiasAuditor, RAGBiasAnalyzer, PipelineTracker, TemporalTracker (CUSUM/EWMA), ActionBiasAnalyzer.
Purpose: Test AI agents for bias in tool selection, retrieval, actions, and delegation. Tracks bias across pipeline stages and detects feedback loops.
11
Multi-Agent Fairness Testing
vfairness.multi_agent
Emergent bias,
groupthink & compositionality
Key APIs: CompositionalityAnalyzer, GroupthinkDetector, EmergentBiasDetector.
Purpose: Detect emergent bias in multi-agent systems. Tests non-compositionality, groupthink convergence, and coalition formation.
Full Library Validation: vfairness_0_library_validation.ipynb — Comprehensive test of all modules & functions

Installation

bash
# Core installation
pip install vfairness

# With visualization support
pip install vfairness[viz]

# With MLflow integration
pip install vfairness[mlflow]

# Full installation (all optional dependencies)
pip install vfairness[all]

Dependencies

Package Required Install Extra Purpose
numpy Core Numerical operations
pandas Core DataFrame operations
scipy Core Statistical tests, distributions, bias detection
jinja2 Optional [rendering] SVG report template engine
matplotlib Optional [viz] Static plots and charts
seaborn Optional [viz] Statistical visualizations
plotly Optional [dashboard] Interactive dashboards and charts
dash Optional [interactive] Live interactive dashboard server
mlflow Optional [mlops] Experiment tracking and model registry
wandb Optional [mlops] Experiment tracking and run logging
pywt Optional [monitoring] Wavelet-based multi-scale drift detection
torch Optional [training] Training-time interventions (loss functions, regularizers)
bash
# Core only (numpy, pandas, scipy)
pip install vfairness

# With visualization
pip install vfairness[viz]

# With SVG rendering + dashboards
pip install vfairness[rendering,dashboard]

# With training-time interventions (PyTorch)
pip install vfairness[training]

# With MLOps integrations (MLflow + W&B)
pip install vfairness[mlops]

# Everything
pip install vfairness[all]

Import Patterns

python
# Recommended: New module-based imports
from vfairness.preprocessing import (
    BiasDetector,
    FeatureEngineeringAnalyzer,
    detect_historical_patterns,
    identify_proxy_variables
)

from vfairness.post_processing import (
    # Calibration
    GroupCalibrator, CalibrationAnalyzer, expected_calibration_error,
    # Threshold Optimization
    GroupThresholdOptimizer, MultiObjectiveThresholdOptimizer,
    ThresholdAnalyzer, FairnessConstraintType,
    # Prediction Reweighting
    PredictionReweighter, RejectionOptionClassifier,
    CalibratedEqualizer, DistributionMatcher, ReweightingAnalyzer
)

from vfairness.evaluation import (
    FairnessAnalyzer,
    demographic_parity_difference,
    classification_fairness_report,
    print_report
)

from vfairness.operations import (
    DataBiasValidator,
    ModelFairnessGate,
    FairnessTestSuite
)

# Flat imports also work
from vfairness import FairnessAnalyzer, BiasDetector, GroupCalibrator

# Feature-specific imports
from vfairness import (
    # Classification metrics
    demographic_parity_difference,
    equalized_odds_difference,
    equal_opportunity_difference,
    predictive_parity_difference,

    # Regression metrics
    mae_parity_difference,
    rmse_parity_difference,
    mean_prediction_difference,

    # Ranking metrics
    exposure_parity_difference,
    attention_weighted_rank_fairness,

    # Reports
    classification_fairness_report,
    regression_fairness_report,
    print_report,

    # Statistical validation
    permutation_test,
    bootstrap_ci,
    compute_effect_sizes,
    proportion_z_test,
    fisher_exact_test,
    cohens_h,
    cohens_h_interpretation,
    minimum_detectable_effect,
    power_warning,

    # Auto-discovery
    detect_protected_attributes,
    identify_proxy_features,
    scan_fairness_violations,

    # MLOps
    log_fairness_to_mlflow,
    assert_fairness
)

1. Data & Preprocessing

Fairness-aware feature transforms and pre-training bias auditing to detect and address bias before it propagates to models.

python
from vfairness.preprocessing import BiasDetector, FeatureEngineeringAnalyzer

What's Included

Bias Detection
Identify historical patterns, representation issues, and statistical disparities in training data
Feature Engineering
Analyze and transform features to reduce discriminatory signals while preserving predictive power
flowchart LR subgraph PP["Data & Preprocessing"] direction TB RAW[Raw Data] --> BD[Bias Detection] BD --> FE[Feature Engineering] FE --> CLEAN[Clean Data] end subgraph BiasDetection["Bias Detection"] HP[Historical Patterns] RB[Representation Bias] SD[Statistical Disparity] PV[Proxy Variables] end subgraph FeatureEng["Feature Engineering"] CA[Correlation Analysis] TR[Transformers] PR[Proxy Reduction] end BD --> BiasDetection FE --> FeatureEng style RAW fill:#fee2e2,stroke:#ef4444 style CLEAN fill:#d1fae5,stroke:#10b981 style PP fill:#fef3c7,stroke:#f59e0b style BiasDetection fill:#fff7ed,stroke:#f59e0b style FeatureEng fill:#fff7ed,stroke:#f59e0b
Click diagram to zoom & pan

Pre-Training Bias Auditing

Bias Detection Module

The Bias Detection module provides comprehensive tools for identifying bias in training data before model deployment. It analyzes historical patterns, representation disparities, statistical distributions, and proxy variables.

Demo Notebook: vfairness_1_bias_detection_demo.ipynb — End-to-end bias detection walkthrough

Data Engineering for Fairness

Bias detection, representation analysis and data preprocessing for fairer ML pipelines

0:00 / 0:00
Click diagram to zoom & pan
flowchart TB
    subgraph BD["Bias Detection Module"]
        BDA["BiasDetector
(Unified Interface)"] subgraph Detection["Detection Components"] HP["Historical Patterns
43 curated patterns"] RB["Representation Bias
Population comparison"] SD["Statistical Disparity
Group differences"] PV["Proxy Variables
Indirect discrimination"] end subgraph Output["Analysis Output"] AR["BiasAuditReport"] CI["Critical Issues"] REC["Recommendations"] end end BDA --> Detection Detection --> Output style BDA fill:#4A90D9,stroke:#333,stroke-width:2px,color:#fff style Detection fill:#fef3c7,stroke:#f59e0b style Output fill:#f0fdf4,stroke:#22c55e

Key Capabilities

Historical Patterns
Features linked to documented discrimination — redlining, SCHUFA, EU AI Act
Representation Analysis
Compares dataset demographics against population benchmarks
Statistical Disparity
Detects outcome and feature distribution differences across groups
Proxy Detection
Finds features that indirectly encode protected attributes

BiasDetector

Unified auditor for comprehensive bias detection in training data.

Background

Bias in training data is the single most common source of unfair model outcomes. BiasDetector addresses four complementary dimensions of data bias: historical patterns (features linked to documented discrimination such as redlining or SCHUFA scoring), representation imbalance (demographic proportions that deviate from population benchmarks), statistical disparity (outcome-rate differences across groups), and proxy variables (features that indirectly encode protected attributes).

The detector produces a BiasAuditReport that assigns a heuristic risk score per module and overall, lists critical issues, and provides actionable recommendations. Running a full audit before model training is the recommended first step in any fairness workflow.

How the Audit Report Looks

The diagram below shows an example BiasAuditReport output. The left panel lists critical issues found by each detection module; the right panel visualises a representation analysis comparing dataset demographics against population benchmarks.

When to Use BiasDetector

Use BiasDetector before model training to catch data-level bias early. For post-training fairness evaluation (comparing predictions against ground truth), use FairnessAnalyzer instead. For CI/CD pipeline integration, combine BiasDetector findings with DataBiasValidator.

class BiasDetector Main Class

Constructor

python
BiasDetector(
    df: pd.DataFrame,
    protected_attributes: List[str],
    *,
    outcome_column: Optional[str] = None,
    feature_columns: Optional[List[str]] = None,
    benchmarks: Optional[Dict[str, Dict[str, float]]] = None,
    config: Optional[Dict[str, Any]] = None,
)
Parameters
df pd.DataFrame Required
DataFrame containing features and optionally the outcome variable.
protected_attributes List[str] Required
List of column names for protected attributes (e.g., ['gender', 'race']).
outcome_column Optional[str] Optional None
Name of the target/outcome column (keyword-only).
feature_columns Optional[List[str]] Optional None
Specific feature columns to analyse. If None, all non-protected, non-outcome columns are used.
benchmarks Optional[Dict[str, Dict[str, float]]] Optional None
Population benchmarks for representation analysis (e.g., {'race': {'White': 0.60, 'Black': 0.13}}).
config Optional[Dict[str, Any]] Optional None
Additional configuration overrides for individual detection modules.

Methods

Method Returns Description
full_audit(**kwargs) BiasAuditReport Run comprehensive bias audit (keyword-only flags: include_historical, include_representation, include_disparities, include_proxies, include_intersectional)
detect_historical_patterns() List[HistoricalPatternResult] Scan for 43 historical discrimination patterns across US, EU, EU AI Act & Swiss jurisdictions
detect_representation_bias() List[RepresentationBiasResult] Analyze group representation
analyze_disparities() List[StatisticalDisparityResult] Statistical disparity analysis
identify_proxies() List[ProxyVariableResult] Find proxy variables

Example

python
from vfairness import BiasDetector
import pandas as pd

# Your data
df = pd.read_csv('loan_applications.csv')

# Define population benchmarks
benchmarks = {
    'gender': {'Male': 0.49, 'Female': 0.51},
    'race': {'White': 0.60, 'Black': 0.13, 'Hispanic': 0.19, 'Asian': 0.06}
}

# Create detector
detector = BiasDetector(
    df,
    protected_attributes=['gender', 'race', 'age'],
    outcome_column='approved',
    benchmarks=benchmarks,
)

# Run full audit
report = detector.full_audit()

# Print summary
print(report.summary())
print(f"Overall Risk Score: {report.overall_risk_score:.1%}")
print(f"Critical Issues: {len(report.critical_issues)}")

# Get high-risk features
for feature in report.get_high_risk_features():
    print(f"  - {feature}")
Risk Score is a Heuristic Limitation

The overall_risk_score uses manually-chosen weights (not empirically validated). Individual module failures may be silently swallowed.

Gap: The aggregate score lacks calibrated interpretability — a score of 0.6 has no standardised meaning across datasets. Some analysis steps use broad except Exception: pass patterns, so errors during computation are not surfaced.
Mitigation: Rely on per-module detailed findings rather than the aggregate score. Cross-check that the number of results matches expectations (e.g., if you have 5 protected attributes, verify you get 5 representation results). Build your own risk scoring framework aligned with your organization's risk tolerance.
Representation Benchmarks Not Auto-Generated Limitation

The benchmarks parameter requires manually-provided population distributions. The library does not fetch from Census APIs, Eurostat, or BFS.

Gap: Without benchmarks, representation analysis falls back to a crude heuristic (flagging groups <10% if another >30%). Default benchmarks are US Census 2020 only, covering binary gender and US racial categories. Non-US, non-binary, and domain-specific populations have no defaults.
Mitigation: Always pass custom benchmarks from authoritative sources for your jurisdiction and domain. Example:
benchmarks={'gender': {'male': 0.49, 'female': 0.48, 'non-binary': 0.03}}

Historical Pattern Detection

Detects features linked to historical discrimination patterns. The library ships with 43 curated patterns spanning four jurisdictions, including EU AI Act (Regulation 2024/1689) compliance detection.

Pattern Catalog Overview

Region Count Risk Levels Key Patterns
US / Global 12 Critical, High, Medium Redlining, credit history, healthcare cost, criminal records, names
European 12 Critical, High, Medium Migration background, SCHUFA scoring, predictive policing, biometrics
EU AI Act 11 4 Critical (Art. 5), 7 High (Annex III) Social scoring, emotion recognition, creditworthiness, recruitment
Swiss 8 Critical, High, Medium Permit system, Betreibungsregister, housing discrimination, Gemeinde

EU AI Act Compliance

Automated Regulatory Flagging

When detect_historical_patterns() finds features matching EU AI Act prohibited practices (Art. 5), they are flagged as CRITICAL risk with penalties up to €35M / 7% turnover. High-risk Annex III matches carry €15M / 3% turnover penalties.

How Historical Detection Works

The function iterates over every column in the DataFrame, normalises the name, and matches it against keyword lists in the 43-pattern catalog. Each matched column is scored and enriched with context before being returned. The full pipeline is:

  1. Normalise column name — the column name is lowercased and transformed two ways: col_lower (underscores/hyphens → spaces) and col_normalized (spaces/hyphens → underscores).
  2. Keyword matching — for each of the 43 patterns, every keyword is tested against the normalised name via substring match or exact normalised match. First keyword hit wins, and each column maps to at most one pattern.
  3. Base confidence score — assigned based on match quality: exact name match (+0.6), long keyword ≥ 6 chars (+0.5), or short keyword (+0.4).
  4. Risk-level boost — CRITICAL patterns get +0.15, HIGH patterns get +0.10.
  5. Value analysis boost (if enabled) — geographic patterns with 10–1 000 unique values get +0.10; name patterns with > 100 unique values get +0.10.
  6. Correlation boost (if protected_attributes provided) — if the Pearson correlation between the encoded feature and any protected attribute exceeds |0.3|, confidence gets +0.20.
  7. Confidence threshold gate — if the final confidence < min_confidence (default 0.3), the match is discarded.
  8. Build result — risk level, affected groups, recommendations, and historical context are copied from the pattern catalog. Evidence dict stores matched keyword, cardinality, sample values, and correlations.
  9. Sort and return — results are sorted by risk level (CRITICAL first), then by descending confidence.

Detection Decision Tree

The diagram below shows the full decision flow for each column in the DataFrame. Click the diagram to zoom & pan.

Click diagram to zoom & pan
flowchart TD START(["detect_historical_patterns(df)"]) START --> MERGE["Merge built-in 43 patterns
+ custom_patterns (if any)"] MERGE --> LOOP["For each column in df"] LOOP --> NORM["Normalise column name
col_lower: _ → space
col_normalized: space → _"] NORM --> SCAN["For each pattern
(43 categories)"] SCAN --> KW{"Keyword
match?"} KW -->|"substring in col_lower
OR exact == col_normalized"| MATCH["✓ Matched
(first keyword wins)"] KW -->|"No match in
any keyword"| NEXT{"More
patterns?"} NEXT -->|"Yes"| SCAN NEXT -->|"No"| SKIPCOL["Column not flagged"] MATCH --> BASE{"Match
quality?"} BASE -->|"Exact name match"| B06["+0.60"] BASE -->|"Long keyword ≥ 6 chars"| B05["+0.50"] BASE -->|"Short keyword < 6 chars"| B04["+0.40"] B06 --> RBOOST B05 --> RBOOST B04 --> RBOOST RBOOST{"Pattern
risk level?"} RBOOST -->|"CRITICAL"| RC["+0.15"] RBOOST -->|"HIGH"| RH["+0.10"] RBOOST -->|"MEDIUM / LOW"| RN["+0.00"] RC --> VCHECK RH --> VCHECK RN --> VCHECK VCHECK{"Value analysis
enabled?"} VCHECK -->|"No"| CORRCHECK VCHECK -->|"Yes"| VTYPE{"Pattern
type?"} VTYPE -->|"Geographic &
10–1000 uniques"| VG["+0.10"] VTYPE -->|"Name &
>100 uniques"| VN["+0.10"] VTYPE -->|"Other"| V0["+0.00"] VG --> CORRCHECK VN --> CORRCHECK V0 --> CORRCHECK CORRCHECK{"Protected attrs
provided?"} CORRCHECK -->|"No"| GATE CORRCHECK -->|"Yes"| CORRCALC["Compute Pearson r
vs each protected attr"] CORRCALC --> CTEST{"|r| > 0.3?"} CTEST -->|"Yes"| CBOOST["+0.20"] CTEST -->|"No"| C0["+0.00"] CBOOST --> GATE C0 --> GATE GATE{"confidence
≥ min_confidence?"} GATE -->|"No"| DISCARD["⛔ Discard match"] GATE -->|"Yes"| BUILD["Build
HistoricalPatternResult"] BUILD --> RESULT(["Add to results list
(risk level, context,
groups, recommendations)"]) SKIPCOL --> NEXTCOL DISCARD --> NEXTCOL RESULT --> NEXTCOL NEXTCOL{"More
columns?"} NEXTCOL -->|"Yes"| LOOP NEXTCOL -->|"No"| SORT(["Sort by risk level ↓
then confidence ↓
Return results"]) style START fill:#e0e7ff,stroke:#6366f1,stroke-width:2px,color:#1e1b4b style SORT fill:#e0e7ff,stroke:#6366f1,stroke-width:2px,color:#1e1b4b style RESULT fill:#f0fdf4,stroke:#22c55e,color:#14532d style MATCH fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f style SKIPCOL fill:#f8fafc,stroke:#94a3b8,color:#475569 style DISCARD fill:#fef2f2,stroke:#ef4444,color:#7f1d1d style BUILD fill:#f0fdf4,stroke:#22c55e,color:#14532d style B06 fill:#fef9c3,stroke:#eab308,color:#713f12 style B05 fill:#fef9c3,stroke:#eab308,color:#713f12 style B04 fill:#fef9c3,stroke:#eab308,color:#713f12 style RC fill:#fff7ed,stroke:#f97316,color:#7c2d12 style RH fill:#fff7ed,stroke:#f97316,color:#7c2d12 style RN fill:#f8fafc,stroke:#94a3b8,color:#475569 style VG fill:#ede9fe,stroke:#8b5cf6,color:#3b0764 style VN fill:#ede9fe,stroke:#8b5cf6,color:#3b0764 style V0 fill:#f8fafc,stroke:#94a3b8,color:#475569 style CBOOST fill:#e0f2fe,stroke:#0aafe3,color:#0c4a6e style C0 fill:#f8fafc,stroke:#94a3b8,color:#475569 style MERGE fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f style NORM fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f

Confidence Score Breakdown

The confidence score (0.0–1.0, capped) is built additively from four independent signal sources:

Stage Condition Points Example
A. Match quality Exact column name = keyword +0.60 Column zip_code matches keyword zip_code
Long keyword ≥ 6 chars (partial) +0.50 Column applicant_zip_code_region contains zip_code
Short keyword < 6 chars (partial) +0.40 Column home_zip contains zip
B. Risk boost Pattern risk = CRITICAL +0.15 Criminal records, migration background, EU AI Act Art. 5
Pattern risk = HIGH +0.10 Redlining, salary history, EU AI Act Annex III
Pattern risk = MEDIUM / LOW +0.00 Neighbourhood names, digital access
C. Value analysis Geographic pattern with 10–1 000 unique values +0.10 zip_code with 250 distinct values
Name pattern with > 100 unique values +0.10 surname with 1 800 unique names
D. Correlation boost |Pearson r| > 0.3 with any protected attribute +0.20 zip_code correlates with race at r = 0.65

Maximum possible score: 1.00 (capped). Minimum to survive the gate: min_confidence (default 0.3).

Functions

detect_historical_patterns

Analyze dataset columns to identify features linked to known patterns of historical discrimination (redlining, segregation, employment discrimination, EU AI Act prohibited practices, etc.).

python
from vfairness.preprocessing.bias_detection import detect_historical_patterns, HISTORICAL_RISK_PATTERNS

results = detect_historical_patterns(
    df,
    protected_attributes=['race', 'gender'],
    min_confidence=0.3,
    include_value_analysis=True
)

for result in results:
    print(f"{result.feature}: {result.risk_level.value.upper()}")
    print(f"  Pattern: {result.pattern_type}")
    print(f"  Context: {result.historical_context}")
    print(f"  Affected groups: {result.affected_groups}")
    print(f"  Recommendations: {result.recommendations}")
Parameters
df pd.DataFrame Required
DataFrame to analyze. All column names are scanned against the pattern keyword catalog.
protected_attributes Optional[List[str]] Optional None
List of known protected attribute columns. Used for correlation analysis between detected features and protected groups.
custom_patterns Optional[Dict[str, Dict]] Optional None
Custom historical patterns to check in addition to the built-in 43 patterns. Must follow the same format as HISTORICAL_RISK_PATTERNS.
min_confidence float Optional 0.3
Minimum confidence threshold (0.0–1.0) for reporting findings. Lower values return more results with weaker matches.
include_value_analysis bool Optional True
Whether to analyze actual column values for additional signals beyond keyword matching (e.g., checking if ZIP codes appear in HOLC-graded areas).
Returns

List[HistoricalPatternResult] — sorted by risk level (critical first). Each result is a dataclass with the fields below.

attribute_historical_pattern

Structured cross-walk from a protected attribute and a regulated domain to the canonical documented historical-discrimination pattern. Used by the Pulse orchestrator to attach a citation-backed historicalPattern envelope onto bias findings so the Validant frontend can route them into the “Historical pattern” channel without keyword-matching evidence text. Citations are pulled by reference from _DOMAIN_HISTORICAL_PRECEDENT; unknown combinations return None (silence beats invention).

python
from vfairness import attribute_historical_pattern

attribute_historical_pattern("race", "lending")
# {
#   "pattern_id": "lending_race_redlining",
#   "label": "Race in lending (redlining)",
#   "summary": "Credit-allocation models inherit decades of redlining; ...",
#   "citations": ["Rothstein (2017), 'The Color of Law'",
#                 "Bartlett et al. (2022), JFE"],
#   "attribute_class": "race",
#   "domain": "lending",
#   "jurisdiction": "",
# }

attribute_historical_pattern("age", "hiring")     # -> hiring_age   (ADEA / EU 2000/78)
attribute_historical_pattern("gender", "insurance")  # -> insurance_gender (Test-Achats 2011)
attribute_historical_pattern("religion", "education")  # -> None
Parameters
attribute Optional[str] Required
Raw attribute / column name. Lower-cased and aliased: Race / Ethnicity, race_ethnicity, skin_colour, sex, gender_identity, age_group, dob, zip, postcode, neighbourhood, census_tract all resolve to their canonical attribute class.
domain Optional[str] Required
Regulated domain. Aliases follow domain_historical_context(): recruitment/employment → hiring, credit/loan → lending, medical/clinical → healthcare, recidivism/criminal → justice. Empty / unknown domains return None.
jurisdiction str Optional ""
Jurisdiction tag carried through into the returned envelope (not used to filter the lookup).
Returns

Optional[Dict[str, Any]] with pattern_id, label, summary, citations, attribute_class, domain, jurisdiction — or None when the combination has no documented precedent.

Coverage (24 documented pairs): race × {hiring, lending, healthcare, justice, insurance, education}; gender × {hiring, lending, healthcare, insurance, education}; age × {hiring, lending, insurance}; national_origin × {hiring, lending}; religion × {hiring, lending}; disability × {hiring, healthcare, insurance}; geographic-proxy (zip / postcode / neighbourhood / census tract) × {lending, insurance, healthcare}.

HistoricalPatternResult Fields

Field Type Description
feature str Column name that matched
risk_level HistoricalRiskLevel Enum: critical, high, medium, low, none
pattern_type str Category (e.g., "PROHIBITED — Emotion Recognition (Art. 5(1)(f))")
description str Human-readable description of the finding
confidence float Match confidence (0.0–1.0)
historical_context str Academic and legal background explaining the risk
affected_groups List[str] Demographics historically impacted
recommendations List[str] Mitigation steps with regulatory references
evidence Dict[str, Any] Supporting evidence for the detection (keyword matched, correlation data, etc.)
Keyword-Based Column Matching Limitation

Pattern detection matches column names against keyword lists via substring search. It does not use NLP, embeddings, or semantic analysis.

Gap: Columns with non-standard names, abbreviations, or non-English names will not be detected. A column named postal_region_id may be missed if only zip and postcode are in the keyword list. Confidence scores are heuristic-based and not probability-calibrated.
Mitigation: Manually review all dataset columns alongside auto-detected results. Use the HISTORICAL_RISK_PATTERNS dict to inspect the exact keywords for each pattern. Rename ambiguous columns to match expected keywords before running detection.

Geographic Discrimination Data

Integration with historical geographic discrimination databases including HOLC redlining maps (1930s–40s) and CDC Social Vulnerability Index.

How Geographic Risk Assessment Works

The core function assess_geographic_feature_risk() evaluates whether a list of geographic identifiers (currently ZIP codes) overlaps with historically discriminatory areas. The pipeline is:

  1. Feature-type gate — only 'zip_code' is supported. Any other type returns immediately with risk_score = 0.
  2. HOLC grade lookup — each value is matched against a built-in ZIP → HOLC grade dictionary covering ~57 ZIP codes across 39 US cities (Atlanta, Baltimore, Birmingham, Boston, Brooklyn, Buffalo, Chicago, Cleveland, Columbus, Dallas, Denver, Detroit, Hartford, Houston, Indianapolis, Jacksonville, Kansas City, Los Angeles, Louisville, Memphis, Miami, Milwaukee, Minneapolis, Nashville, New Orleans, New York, Oakland, Philadelphia, Pittsburgh, Portland, Providence, Richmond, Sacramento, San Antonio, San Francisco, Seattle, St. Louis, Tampa, Washington DC). Unmatched ZIPs are counted as Unknown.
  3. Coverage calculationholc_coverage = known_count / total, where known_count excludes Unknown.
  4. Weighted risk score — each grade carries a weight: D = 1.0, C = 0.7, B = 0.3, A = 0.0. The score is the weighted average across all values.
  5. Disparate impact level — derived from the risk score using fixed thresholds (see table below).
  6. Recommendations — generated conditionally based on the risk level and HOLC coverage.

Risk Assessment Decision Tree

The diagram below shows the full decision flow of assess_geographic_feature_risk(). Click the diagram to zoom & pan.

Click diagram to zoom & pan
flowchart TD START(["assess_geographic_feature_risk(values)"]) START --> FTYPE{"feature_type
== 'zip_code'?"} FTYPE -->|"No"| UNSUP["⛔ Return risk_score=0
disparate_impact='unknown'"] FTYPE -->|"Yes"| EMPTY{"values
empty?"} EMPTY -->|"Yes"| NOVAL["⛔ Return risk_score=0
'No values to analyze'"] EMPTY -->|"No"| LOOKUP["For each ZIP:
lookup_holc_grade_by_zip()"] LOOKUP --> COUNT["Count grades:
A, B, C, D, Unknown"] COUNT --> COV["holc_coverage =
(A+B+C+D) / total"] COV --> WSCORE["Weighted risk score =
Σ(count × weight) / total"] WSCORE --> WEIGHTS["Weights:
D=1.0 · C=0.7 · B=0.3 · A=0.0"] WEIGHTS --> AFFECTED["affected_samples =
count(C) + count(D)"] AFFECTED --> RISK{"risk_score
threshold?"} RISK -->|"≥ 0.5"| CRIT["🚨 CRITICAL"] RISK -->|"0.3 – 0.5"| HIGH["🔴 HIGH"] RISK -->|"0.1 – 0.3"| MED["🟡 MEDIUM"] RISK -->|"< 0.1"| LOW["🟢 LOW"] CRIT --> RECS HIGH --> RECS MED --> RECS LOW --> RECS RECS{"Generate
recommendations"} RECS --> RECD{"Any Grade D
samples?"} RECD -->|"Yes"| RECWARN["⚠️ '{pct}% from redlined
Grade D areas'"] RECD -->|"No"| RECLEVEL RECWARN --> RECLEVEL RECLEVEL{"Risk level?"} RECLEVEL -->|"critical / high"| RECCH["'Remove/aggregate feature'
'Test for geo disparities'
'Apply fairness constraints'"] RECLEVEL -->|"medium"| RECM["'Monitor for disparities'
'Consider broader aggregation'"] RECLEVEL -->|"low"| RECL["(no extra recs)"] RECCH --> COVCHECK RECM --> COVCHECK RECL --> COVCHECK COVCHECK{"holc_coverage
< 50%?"} COVCHECK -->|"Yes"| COVREC["⚠️ 'Supplement with
other risk indicators'"] COVCHECK -->|"No"| RESULT COVREC --> RESULT RESULT(["GeographicRiskAssessment"]) style START fill:#e0e7ff,stroke:#6366f1,stroke-width:2px,color:#1e1b4b style RESULT fill:#e0e7ff,stroke:#6366f1,stroke-width:2px,color:#1e1b4b style UNSUP fill:#fef2f2,stroke:#ef4444,color:#7f1d1d style NOVAL fill:#fef2f2,stroke:#ef4444,color:#7f1d1d style LOOKUP fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f style COUNT fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f style COV fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f style WSCORE fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f style WEIGHTS fill:#ede9fe,stroke:#8b5cf6,color:#3b0764 style CRIT fill:#fef2f2,stroke:#ef4444,stroke-width:2px,color:#7f1d1d style HIGH fill:#fff7ed,stroke:#f97316,color:#7c2d12 style MED fill:#fef9c3,stroke:#eab308,color:#713f12 style LOW fill:#f0fdf4,stroke:#22c55e,color:#14532d style RECWARN fill:#fef3c7,stroke:#f59e0b,color:#78350f style RECCH fill:#f0fdf4,stroke:#22c55e,color:#14532d style RECM fill:#f0fdf4,stroke:#22c55e,color:#14532d style COVREC fill:#fef3c7,stroke:#f59e0b,color:#78350f

HOLC Redlining Grades

The Home Owners' Loan Corporation (HOLC) graded neighbourhoods in US cities between 1935–1940. These grades systematically disadvantaged minority communities and their effects persist today.

Grade Colour HOLC Label Description Weight
A Green "Best" Predominantly white, affluent neighbourhoods deemed safest for lending. 0.0
B Blue "Still Desirable" Generally white, middle-class areas considered stable. 0.3
C Yellow "Definitely Declining" Immigrant and mixed-race areas flagged as risky for investment. 0.7
D Red "Hazardous" Predominantly Black / minority neighbourhoods — redlined. Banks refused mortgages. 1.0

Risk Score Thresholds

The weighted risk score is mapped to a disparate impact level using the following thresholds:

Risk Level Score Range Interpretation
CRITICAL ≥ 0.5 Majority of samples from Grade C/D areas. Feature very likely perpetuates redlining effects.
HIGH 0.3 – 0.5 Significant overlap with historically discriminated areas. Strongly consider removing the feature.
MEDIUM 0.1 – 0.3 Some overlap detected. Monitor model outcomes for geographic disparities.
LOW < 0.1 Minimal overlap with redlined areas. Low risk of perpetuating geographic discrimination.

CDC Social Vulnerability Index Themes

The SVI provides contemporary area-level vulnerability data for the entire United States. Four themes are available via SVI_THEMES:

Theme Indicators
Socioeconomic Status Below poverty line, unemployed, low income, no high school diploma
Household Composition & Disability Aged 65+, under 17, disability, single-parent households
Minority Status & Language Minority population, limited English proficiency
Housing & Transportation Multi-unit structures, mobile homes, crowding, no vehicle, group quarters

GeographicRiskAssessment Fields

Field Type Description
feature_namestrType of geographic feature assessed (e.g. 'zip_code').
risk_scorefloatWeighted risk score (0.0–1.0) based on HOLC grade distribution.
holc_coveragefloatFraction of input values that could be matched to HOLC grades.
grade_distributionOptional[Dict]Proportion of values in each HOLC grade (A, B, C, D, Unknown).
affected_samplesintCount of values in Grade C or D areas.
disparate_impact_riskstrRisk level: 'critical', 'high', 'medium', or 'low'.
recommendationsList[str]Context-specific remediation advice.
data_sources_usedList[str]Data sources used (e.g. ['HOLC Redlining Maps (Mapping Inequality Project)']).

Data Quality Notes

Key limitations of the geographic data
  • Temporal: HOLC maps are 80+ years old. Demographics have changed, but discriminatory effects persist in property values, school funding, and environmental exposure.
  • Spatial precision: HOLC boundaries are hand-drawn and do not align with ZIP codes or census tracts. The ZIP-to-grade mapping is an approximation.
  • Coverage: Only ~57 ZIP codes across 39 US cities are in the built-in database. Rural areas were never surveyed.
  • Recommendation: Use HOLC data as one signal among many. Supplement with contemporary data (CDC SVI, ACS, HMDA) and validate locally with domain experts.

Functions

get_available_holc_cities

List all US cities with available HOLC redlining map data from the Mapping Inequality project.

python
get_available_holc_cities() -> List[str]
Parameters
 
No parameters. Returns the built-in list of cities.
Returns

List[str] — City names with HOLC data in City_State format (e.g., ['Atlanta_GA', 'Baltimore_MD', 'Birmingham_AL', 'Boston_MA', ...]). 39 cities total.

lookup_holc_grade_by_zip

Look up the approximate HOLC redlining grade for a US ZIP code.

python
lookup_holc_grade_by_zip(
    zip_code: str
) -> Optional[HOLCGrade]
Parameters
zip_code str Required
US ZIP code to look up (e.g., '48201').
Returns

Optional[HOLCGrade] — HOLC grade enum (A="Best", B="Still Desirable", C="Declining", D="Hazardous") or None if ZIP not in database.

assess_geographic_feature_risk

Assess whether a geographic feature (e.g., ZIP codes) encodes disparate impact risk based on historical redlining data.

python
assess_geographic_feature_risk(
    values: List[str],
    feature_type: str = 'zip_code'
) -> GeographicRiskAssessment
Parameters
values List[str] Required
List of geographic identifiers (e.g., ZIP codes) from the dataset.
feature_type str Optional 'zip_code'
Type of geographic feature. Currently supports 'zip_code'.
Returns

GeographicRiskAssessment — includes risk_score, holc_coverage, grade_distribution, affected_samples, disparate_impact_risk, and recommendations.

CDC Social Vulnerability Index

python
from vfairness.preprocessing.bias_detection import SVI_THEMES, get_svi_data_url

# View available themes
for theme, desc in SVI_THEMES.items():
    print(f"{theme}: {desc}")

# Get download URL for SVI data
url = get_svi_data_url(year=2022)
US-Only Coverage

HOLC data covers approximately 40 US cities and ~100 ZIP codes. The CDC SVI is also US-only. For non-US jurisdictions, build custom geographic risk datasets using sources such as Eurostat NUTS regions, Swiss BFS Gemeindetypologie, or UK Index of Multiple Deprivation.

Representation Bias Detection

Detects under- and overrepresentation of demographic groups by comparing dataset distributions against population benchmarks.

How Representation Detection Works

The function analyses each protected attribute independently, then optionally checks intersectional combinations. For every attribute the pipeline is:

  1. Resolve the benchmark — if the caller supplies a benchmarks dict, use it; otherwise fall back to the built-in US Census 2020 approximate defaults (gender, race, age_group). If neither exists, a uniform-distribution heuristic is used.
  2. Compute actual distributionvalue_counts(normalize=True) after dropping NaN values.
  3. Compute representation ratios — for each group: ratio = actual_proportion / expected_proportion.
  4. Flag underrepresented groups — groups with ratio < underrepresentation_threshold (default 0.8). The deficit in absolute sample count is also computed.
  5. Flag overrepresented groups — groups with ratio > overrepresentation_threshold (default 1.2).
  6. Chi-squared goodness-of-fit test — compares observed vs expected counts using scipy.stats.chisquare. Returns (None, None) if fewer than 2 common groups or any expected count ≤ 0.
  7. Determine severity — based on the lowest ratio among underrepresented groups (see thresholds table below).
  8. Generate recommendations — severity-specific remediation advice, plus lists of underrepresented and overrepresented group names.
  9. Intersectional analysis (if enabled and ≥ 2 attributes) — creates compound groups (e.g. black_female), computes expected proportions under independence (product of marginals), flags groups with ratio < threshold or ratio < 0.5.

Detection Decision Tree

The diagram below shows the full decision flow for each protected attribute. Click the diagram to zoom & pan.

Click diagram to zoom & pan
flowchart TD START(["detect_representation_bias(df)"]) START --> LOOP["For each protected attribute"] LOOP --> EXISTS{"Column
in df?"} EXISTS -->|"No"| SKIP["⛔ Skip attribute"] EXISTS -->|"Yes"| BENCH{"Benchmark
available?"} BENCH -->|"User-provided"| USERBENCH["Use caller's
benchmark dict"] BENCH -->|"Not provided"| FALLBACK{"Built-in
default?"} FALLBACK -->|"Yes (gender,
race, age_group)"| DEFAULTBENCH["Use US Census
2020 approx."] FALLBACK -->|"No"| UNIFORM["Use uniform
1 / num_groups"] USERBENCH --> DIST DEFAULTBENCH --> DIST UNIFORM --> NODIST DIST["Compute actual distribution
value_counts(normalize=True)"] DIST --> RATIO["For each group:
ratio = actual / expected"] RATIO --> UNDER{"ratio <
under_threshold?"} UNDER -->|"Yes"| UFLAG["Flag underrepresented
+ compute deficit"] UNDER -->|"No"| OVER{"ratio >
over_threshold?"} OVER -->|"Yes"| OFLAG["Flag overrepresented
+ compute surplus"] OVER -->|"No"| OK["✓ Within
acceptable range"] UFLAG --> CHI OFLAG --> CHI OK --> CHI CHI["Chi-squared
goodness-of-fit test"] CHI --> CHIOK{"≥ 2 groups &
expected > 0?"} CHIOK -->|"Yes"| CHIRES["Compute χ² statistic
+ p-value"] CHIOK -->|"No"| CHINONE["χ² = None
p-value = None"] CHIRES --> SEV CHINONE --> SEV SEV{"Lowest underrep.
ratio?"} SEV -->|"< 0.5"| SEVCRIT["🚨 CRITICAL"] SEV -->|"0.5 – 0.7"| SEVHIGH["🔴 HIGH"] SEV -->|"0.7 – 0.8"| SEVMED["🟡 MEDIUM"] SEV -->|"≥ 0.8 but flagged"| SEVLOW["🟢 LOW"] SEV -->|"No underrep."| SEVOK["✓ ADEQUATE"] SEVCRIT --> RECS SEVHIGH --> RECS SEVMED --> RECS SEVLOW --> RECS SEVOK --> RECS RECS["Generate severity-specific
recommendations"] RECS --> RESULT(["RepresentationBiasResult"]) NODIST["No-benchmark fallback:
check for severe imbalance"] NODIST --> IMBAL{"Any group < 10%
AND any > 40%?"} IMBAL -->|"Yes"| IMFLAG["Flag small groups
as underrepresented
(vs uniform dist.)"] IMBAL -->|"No"| IMOK["No flags"] IMFLAG --> SEV IMOK --> SEV RESULT --> INTER{"include_intersectional
AND ≥ 2 attrs?"} INTER -->|"No"| DONE INTER -->|"Yes"| ICOMB["Create compound groups
(e.g. black_female)"] ICOMB --> IEXP["Expected = product
of marginal proportions"] IEXP --> IRATIO["ratio = actual / expected"] IRATIO --> ICHECK{"ratio < threshold
OR ratio < 0.5?"} ICHECK -->|"Yes"| IFLAG["Add intersectional finding
(critical if ratio < 0.3)"] ICHECK -->|"No"| ISKIP["Not flagged"] IFLAG --> IATTACH["Attach top 10 findings
to first matching result"] ISKIP --> IATTACH IATTACH --> DONE DONE(["Return results list"]) style START fill:#e0e7ff,stroke:#6366f1,stroke-width:2px,color:#1e1b4b style DONE fill:#e0e7ff,stroke:#6366f1,stroke-width:2px,color:#1e1b4b style RESULT fill:#f0fdf4,stroke:#22c55e,color:#14532d style SKIP fill:#fef2f2,stroke:#ef4444,color:#7f1d1d style DIST fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f style RATIO fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f style NODIST fill:#fef9c3,stroke:#eab308,color:#713f12 style CHI fill:#ede9fe,stroke:#8b5cf6,color:#3b0764 style CHIRES fill:#ede9fe,stroke:#8b5cf6,color:#3b0764 style CHINONE fill:#f8fafc,stroke:#94a3b8,color:#475569 style UFLAG fill:#fef2f2,stroke:#ef4444,color:#7f1d1d style OFLAG fill:#fff7ed,stroke:#f97316,color:#7c2d12 style OK fill:#f0fdf4,stroke:#22c55e,color:#14532d style SEVCRIT fill:#fef2f2,stroke:#ef4444,stroke-width:2px,color:#7f1d1d style SEVHIGH fill:#fff7ed,stroke:#f97316,color:#7c2d12 style SEVMED fill:#fef9c3,stroke:#eab308,color:#713f12 style SEVLOW fill:#f0fdf4,stroke:#22c55e,color:#14532d style SEVOK fill:#f0fdf4,stroke:#22c55e,color:#14532d style USERBENCH fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f style DEFAULTBENCH fill:#fef9c3,stroke:#eab308,color:#713f12 style UNIFORM fill:#fef9c3,stroke:#eab308,color:#713f12 style RECS fill:#f0fdf4,stroke:#22c55e,color:#14532d style ICOMB fill:#e0f2fe,stroke:#0aafe3,color:#0c4a6e style IEXP fill:#e0f2fe,stroke:#0aafe3,color:#0c4a6e style IFLAG fill:#e0f2fe,stroke:#0aafe3,color:#0c4a6e style IMFLAG fill:#fef2f2,stroke:#ef4444,color:#7f1d1d

Severity Thresholds

Severity is determined by the lowest representation ratio among all underrepresented groups for a given attribute.

Severity Ratio Range Meaning Example
CRITICAL < 0.5 Group has less than half the expected representation. Severe risk of model blindness. Expected 13% Black, actual 4% → ratio 0.31
HIGH 0.5 – 0.7 Significant underrepresentation. Model performance likely degraded for this group. Expected 19% Hispanic, actual 11% → ratio 0.58
MEDIUM 0.7 – 0.8 Moderate gap. May affect fairness metrics at the margins. Expected 6% Asian, actual 4.5% → ratio 0.75
LOW 0.8 – threshold Slight underrepresentation within acceptable monitoring range. Expected 51% Female, actual 45% → ratio 0.88
ADEQUATE ≥ threshold No underrepresented groups detected. Distribution within acceptable bounds. All groups within 80–120% of benchmark

Intersectional Analysis

When include_intersectional=True and at least 2 protected attributes are provided, the function creates compound groups (e.g. black_female) and checks whether they are represented as expected under statistical independence.

  1. Concatenate attribute values into a single column (e.g. black_female).
  2. Compute marginal proportions for each attribute independently.
  3. For each compound group with at least min_group_size rows, compute expected = marginal_A × marginal_B.
  4. Flag if ratio < underrepresentation_threshold or ratio < 0.5 (hard floor). Severity: critical if ratio < 0.3, else high.
  5. Return the top 10 most underrepresented intersectional groups, sorted by ratio ascending.
Independence assumption
Expected intersectional proportions are computed as the product of marginals, assuming the two attributes are statistically independent. In practice, attributes often correlate (e.g. race × income), so the expected proportion may not reflect the true population. Treat intersectional findings as screening flags, not ground truth.

RepresentationBiasResult Fields

Field Type Description
attributestrProtected attribute name analysed.
severityRepresentationSeverityEnum: critical, high, medium, low, adequate, overrepresented.
group_distributionsDict[str, float]Actual proportion of each group in the dataset.
representation_ratiosDict[str, float]Ratio of actual to expected proportion per group.
underrepresented_groupsList[Dict]Groups below the threshold, with group, ratio, actual_proportion, expected_proportion, deficit.
overrepresented_groupsList[Dict]Groups above the upper threshold, with surplus count.
sample_sizeintTotal non-NaN rows analysed.
chi_squared_statisticOptional[float]χ² goodness-of-fit statistic (None if test could not run).
chi_squared_pvalueOptional[float]p-value from the χ² test.
recommendationsList[str]Severity-specific remediation advice.
intersectional_findingsList[Dict]Top 10 underrepresented compound groups (if enabled).
benchmark_sourceOptional[str]Label describing the benchmark origin (e.g., 'user_provided', 'us_census_2020', or None).

Functions

detect_representation_bias

Compare group distributions against expected population benchmarks.

python
from vfairness.preprocessing.bias_detection import detect_representation_bias

results = detect_representation_bias(
    df,
    protected_attributes=['race', 'gender'],
    benchmarks={
        'race': {'white': 0.60, 'black': 0.13, 'hispanic': 0.19, 'asian': 0.06},
        'gender': {'male': 0.49, 'female': 0.51}
    },
    underrepresentation_threshold=0.8,
    include_intersectional=True
)
Parameters
df pd.DataFrame Required
Input dataset containing the columns to analyse.
protected_attributes List[str] Required
Column names of protected attributes to check for representation bias.
benchmarks Optional[Dict] Optional US Census 2020
Expected population proportions per attribute. Falls back to approximate US Census 2020 defaults if omitted.
underrepresentation_threshold float Optional 0.8
Ratio below which a group is flagged as underrepresented.
include_intersectional bool Optional True
Also analyse intersectional groups (e.g., Black × Female).
Returns

List[RepresentationBiasResult] — one result per protected attribute with group distributions, representation ratios, chi-squared test, and intersectional findings.

compare_to_benchmark

Detailed comparison of a single attribute's distribution against a specific benchmark.

python
compare_to_benchmark(
    df: pd.DataFrame,
    attribute: str,
    benchmark: Dict[str, float],
    *,
    visualize: bool = False
) -> Dict[str, Any]
Parameters
df pd.DataFrame Required
DataFrame containing the attribute column to analyze.
attribute str Required
Column name of the protected attribute to compare.
benchmark Dict[str, float] Required
Expected distribution as {group: proportion}. Must sum to ≈1.0.
visualize bool Optional False
Whether to generate a visual comparison chart.
Returns

Dict[str, Any] — Contains representation_ratios, chi_squared_statistic, p_value, statistically_significant, and per-group deviation details.

Statistical Disparity Analysis

Comprehensive statistical analysis of outcome disparities across protected attributes, combining multiple statistical tests with effect size measurement and multiple-testing correction.

Disparity Types

Every result returned by analyze_statistical_disparities() carries a disparity_type field from the DisparityType enum. The type tells you what kind of inequality was detected and guides your remediation strategy.

Disparity Type Enum Value What it tests Example Typical Action
Outcome DisparityType.OUTCOME Are decision / prediction rates different across demographic groups? Tests columns you specify as outcome_columns (or auto-detected columns whose names contain keywords like approved, score, prediction). Loan approval rate is 72% for group A vs 54% for group B. Apply fairness constraints, prediction-time calibration, or threshold adjustment.
Distribution DisparityType.DISTRIBUTION Do input feature distributions differ across demographic groups? Tests columns that are neither outcomes nor protected attributes — all remaining numeric or low-cardinality categorical features (auto-detected via feature_columns). Mean credit score is 720 for group A vs 650 for group B. Evaluate whether the feature encodes historical bias; consider removing, reweighting, or fair-aware feature engineering.
Quality DisparityType.QUALITY Does data quality (missing rates, error rates) differ across groups? Enabled when include_quality_analysis=True. For each column, compares the missing-value rate across demographic groups using a z-test for proportions. Column income is missing for 2% of group A but 14% of group B. Fix upstream data collection bias; use group-aware imputation strategies.
Intersectional DisparityType.INTERSECTIONAL Are there disparities at intersectional subgroup level (e.g., race × gender)? Enabled when include_intersectional=True and at least 2 protected attributes are given. Creates compound groups and tests outcomes across them. Approval rate for black + female is 41% vs 78% for white + male. Targeted review of intersectional subgroups; ensure no compound disadvantage is hidden by single-axis analysis.
Column Classification Logic

When outcome_columns or feature_columns are None, the function auto-detects them:
Outcomes — columns whose names contain keywords like outcome, approved, score, rating, target, prediction, etc. (up to 10 columns).
Features — all remaining numeric columns plus categorical columns with < 50 unique values (up to 20 columns).
Protected attribute columns are always excluded from both sets.

Test Selection Decision Tree

The function automatically selects the appropriate statistical test based on the data type, number of groups, and the analysis phase. The diagram below shows the full decision flow.

Click diagram to zoom & pan
flowchart TD START(["analyze_statistical_disparities()"]) START --> COLS{"Column
classification"} COLS -->|"outcome_columns
(explicit or auto-detected)"| OC["Outcome columns"] COLS -->|"feature_columns
(explicit or auto-detected)"| FC["Feature columns"] COLS -->|"include_quality_analysis=True"| QC["All columns
(missing rate check)"] COLS -->|"include_intersectional=True
≥ 2 protected attrs"| IC["Outcome columns
× attribute pairs"] OC -->|"DisparityType.OUTCOME"| DT1{"Data type?"} FC -->|"DisparityType.DISTRIBUTION"| DT2{"Data type?"} QC -->|"DisparityType.QUALITY"| ZPROP["Z-test for proportions
(missing rate per group)"] IC -->|"DisparityType.INTERSECTIONAL"| COMPOUND["Create compound groups
(e.g. race_gender)"] COMPOUND --> DT3{"Data type?"} DT1 --> NUM1{"Numeric &
non-binary?"} DT2 --> NUM2{"Numeric &
non-binary?"} DT3 --> NUM3{"Numeric &
non-binary?"} NUM1 -->|"Yes"| GRP1{"# groups?"} NUM1 -->|"No (binary /
categorical)"| CHI1["Chi-squared test
+ Cramér's V"] NUM2 -->|"Yes"| GRP2{"# groups?"} NUM2 -->|"No (binary /
categorical)"| CHI2["Chi-squared test
+ Cramér's V"] NUM3 -->|"Yes"| GRP3{"# groups?"} NUM3 -->|"No (binary /
categorical)"| CHI3["Chi-squared test
+ Cramér's V"] GRP1 -->|"= 2"| TT1["Independent t-test
+ Cohen's d"] GRP1 -->|"≥ 3"| AN1["One-way ANOVA
+ Eta-squared (η²)"] GRP2 -->|"= 2"| TT2["Independent t-test
+ Cohen's d"] GRP2 -->|"≥ 3"| AN2["One-way ANOVA
+ Eta-squared (η²)"] GRP3 -->|"= 2"| TT3["Independent t-test
+ Cohen's d"] GRP3 -->|"≥ 3"| AN3["One-way ANOVA
+ Eta-squared (η²)"] ZPROP --> CORRECT CHI1 --> CORRECT CHI2 --> CORRECT CHI3 --> CORRECT TT1 --> CORRECT TT2 --> CORRECT TT3 --> CORRECT AN1 --> CORRECT AN2 --> CORRECT AN3 --> CORRECT CORRECT{"Multiple-testing
correction"} CORRECT -->|"'fdr_bh'"| FDR["Benjamini-Hochberg
FDR correction"] CORRECT -->|"'bonferroni'"| BONF["Bonferroni
correction"] CORRECT -->|"'none' / None"| NOCORR["No correction"] FDR --> FILTER BONF --> FILTER NOCORR --> FILTER FILTER(["Filter: keep only
significant results
(p < α after correction)"]) style START fill:#e0e7ff,stroke:#6366f1,stroke-width:2px,color:#1e1b4b style FILTER fill:#e0e7ff,stroke:#6366f1,stroke-width:2px,color:#1e1b4b style OC fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f style FC fill:#fef3c7,stroke:#f59e0b,color:#78350f style QC fill:#e0f2fe,stroke:#0aafe3,color:#0c4a6e style IC fill:#ede9fe,stroke:#8b5cf6,color:#3b0764 style TT1 fill:#f0fdf4,stroke:#22c55e,color:#14532d style TT2 fill:#f0fdf4,stroke:#22c55e,color:#14532d style TT3 fill:#f0fdf4,stroke:#22c55e,color:#14532d style AN1 fill:#f0fdf4,stroke:#22c55e,color:#14532d style AN2 fill:#f0fdf4,stroke:#22c55e,color:#14532d style AN3 fill:#f0fdf4,stroke:#22c55e,color:#14532d style CHI1 fill:#f0fdf4,stroke:#22c55e,color:#14532d style CHI2 fill:#f0fdf4,stroke:#22c55e,color:#14532d style CHI3 fill:#f0fdf4,stroke:#22c55e,color:#14532d style ZPROP fill:#f0fdf4,stroke:#22c55e,color:#14532d style FDR fill:#fff7ed,stroke:#f97316,color:#7c2d12 style BONF fill:#fff7ed,stroke:#f97316,color:#7c2d12 style NOCORR fill:#fff7ed,stroke:#f97316,color:#7c2d12

Effect Size Interpretation

Effect sizes quantify the practical magnitude of a disparity, independent of sample size. The function selects the appropriate effect size measure based on the test used.

Effect Size Used When Negligible Small Medium Large
Cohen's d t-test (2 groups, continuous) |d| < 0.2 0.2 – 0.5 0.5 – 0.8 |d| ≥ 0.8
Eta-squared (η²) ANOVA (≥ 3 groups, continuous) η² < 0.01 0.01 – 0.06 0.06 – 0.14 η² ≥ 0.14
Cramér's V Chi-squared (categorical / binary) V < 0.1 0.1 – 0.3 0.3 – 0.5 V ≥ 0.5
Odds Ratio Binary outcome, 2 groups (via compute_effect_sizes) ≈ 1.0 0.67 – 1.5 0.5 – 2.0 < 0.5 or > 2.0
Proportion diff. Quality analysis (missing rate gap) < 5% — not tested > 10%

Significance Levels

Level Enum Value P-value Threshold
Highly Significant SignificanceLevel.HIGHLY_SIGNIFICANT p < 0.001
Significant SignificanceLevel.SIGNIFICANT p < 0.01
Marginally Significant SignificanceLevel.MARGINALLY_SIGNIFICANT p < 0.05
Not Significant SignificanceLevel.NOT_SIGNIFICANT p ≥ 0.05

Functions

analyze_statistical_disparities

Run comprehensive statistical disparity analysis across protected attributes. Automatically classifies columns into outcomes, features, and quality checks, selects appropriate statistical tests, and applies multiple-testing correction. See the decision tree above for the full test selection logic.

python
analyze_statistical_disparities(
    df: pd.DataFrame,
    protected_attributes: List[str],
    outcome_columns: Optional[List[str]] = None,
    feature_columns: Optional[List[str]] = None,
    *,
    significance_level: float = 0.05,
    min_group_size: int = 30,
    include_quality_analysis: bool = True,
    include_intersectional: bool = True,
    correction_method: str = 'fdr_bh'
) -> List[StatisticalDisparityResult]
Parameters
df pd.DataFrame Required
DataFrame containing outcomes, features, and protected attributes.
protected_attributes List[str] Required
Column names of protected attributes to analyze disparities across.
outcome_columns Optional[List[str]] Optional None
Specific outcome columns to test. If None, auto-detected by matching column names against keywords such as approved, score, rating, target, prediction (up to 10 columns). These are tested with DisparityType.OUTCOME.
feature_columns Optional[List[str]] Optional None
Feature columns to include in disparity analysis. If None, all non-protected, non-outcome numeric columns plus categoricals with < 50 unique values are used (up to 20 columns). These are tested with DisparityType.DISTRIBUTION.
significance_level float Optional 0.05
Statistical significance threshold (α) used for filtering results and as the family-wise error rate for multiple-testing correction.
min_group_size int Optional 30
Minimum group size for inclusion. Groups with fewer than this many observations are silently excluded from all tests.
include_quality_analysis bool Optional True
Whether to analyse data quality disparities (differential missing rates across groups). Results carry DisparityType.QUALITY.
include_intersectional bool Optional True
Whether to create compound groups from pairs of protected attributes and test outcomes across them. Results carry DisparityType.INTERSECTIONAL. Requires ≥ 2 protected attributes.
correction_method str Optional 'fdr_bh'
Multiple-testing correction method applied after all individual tests: 'fdr_bh' (Benjamini-Hochberg — controls false discovery rate), 'bonferroni' (conservative, controls family-wise error rate), or None / 'none' for no correction.
Returns

List[StatisticalDisparityResult] — Only statistically significant results (after correction) are returned, sorted by ascending p-value and descending effect size. Each result contains: feature, protected_attribute, disparity_type (see types), test_name, test_statistic, pvalue, significance, effect_size, effect_size_type, effect_interpretation, confidence_interval, group_statistics, privileged_group, disadvantaged_group, disparity_magnitude, sample_sizes, and recommendations.

run_disparity_tests

Convenience function for running a single disparity test between a target column and a protected attribute. Unlike analyze_statistical_disparities(), this tests exactly one column with no auto-detection, no multiple-testing correction, and returns a plain dictionary rather than a list of result objects.

python
run_disparity_tests(
    df: pd.DataFrame,
    target: str,
    protected_attribute: str,
    *,
    test_type: str = 'auto'
) -> Dict[str, Any]
Parameters
df pd.DataFrame Required
DataFrame to analyse.
target str Required
Column name to test for disparity (outcome or feature).
protected_attribute str Required
Column defining demographic groups.
test_type str Optional 'auto'
Statistical test to use: 'auto' (detect from data type), 'ttest', 'anova', 'chi2', or 'mannwhitney'.
Returns

Dict[str, Any] — Dictionary with keys: feature, protected_attribute, disparity_type, test_name, test_statistic, pvalue, significance, effect_size, effect_size_type, effect_interpretation, confidence_interval, group_statistics, privileged_group, disadvantaged_group, disparity_magnitude, sample_sizes, recommendations. Returns {'error': '...', 'target': '...'} if the test cannot be run.

compute_effect_sizes

Compute multiple effect size measures for group differences on a single column. Automatically selects the appropriate measures based on data type: Cohen's d and eta-squared for continuous data, Cramér's V and odds ratio for categorical/binary data. See the effect size interpretation table for threshold values.

python
compute_effect_sizes(
    df: pd.DataFrame,
    target_column: str,
    protected_attribute: str
) -> Dict[str, Any]
Parameters
df pd.DataFrame Required
DataFrame to analyse.
target_column str Required
Column to measure effect sizes for.
protected_attribute str Required
Column defining demographic groups.
Returns

Dict[str, Any] — Dictionary with target, protected_attribute, and effect_sizes sub-dictionary. For continuous targets: cohens_d (2 groups) and eta_squared. For categorical targets: cramers_v and odds_ratio (binary 2-group only). Each entry has value (float) and interpretation (string).

StatisticalDisparityResult

Dataclass returned by analyze_statistical_disparities(). Each instance represents one statistically significant finding.

Field Type Description
featurestrColumn name tested.
protected_attributestrProtected attribute used for grouping.
disparity_typeDisparityTypeOne of OUTCOME, DISTRIBUTION, QUALITY, INTERSECTIONAL.
test_namestrName of statistical test used (e.g. "Independent t-test", "Chi-squared test").
test_statisticfloatTest statistic value.
pvaluefloatP-value (after correction if correction_method was set).
significanceSignificanceLevelInterpreted significance level.
effect_sizefloatEstimated effect size.
effect_size_typestr"Cohen's d", "Eta-squared", "Cramér's V", or "Proportion difference".
effect_interpretationEffectSizeInterpretationNEGLIGIBLE, SMALL, MEDIUM, or LARGE.
confidence_intervalTuple[float, float]95% CI for the effect (mean diff or proportion diff). (None, None) for multi-group tests.
group_statisticsDict[str, Dict]Per-group stats: mean, std, median, n (continuous) or positive_rate, n (binary).
privileged_groupstrGroup with the higher mean / positive rate.
disadvantaged_groupstrGroup with the lower mean / positive rate.
disparity_magnitudefloatAbsolute difference between privileged and disadvantaged groups.
sample_sizesDict[str, int]Number of observations per group.
recommendationsList[str]Actionable recommendations based on disparity type and severity.

Schema Typology (Pre-Flight Gate)

detect_protected_attributes() only finds protected columns. classify_column_roles() classifies every column so the engine knows what may legitimately feed a model, what is the answer key, and what must never enter a model at all. It is the pre-flight gate behind Pulse's schema check and the Navigator's profiling step.

RoleMeaning
identity_piiNames, email, phone, address, photo, date of birth, near-unique IDs — must never be a feature.
oracleThe ground-truth / target answer key — scored on, never a feature.
model_outputThe model's own decision / score / probability.
protectedA protected attribute (the user's declaration is authoritative).
proxy_candidateCorrelates with a protected attribute, or a known proxy-shaped name (zip, university tier, tenure gap) — kept as a feature but flagged.
job_relevantA legitimate predictor with no PII / protected / output signal.
unknownConstant / empty column.
Why this matters (business / compliance)
This is the answer to "we don't even collect race" (it finds the proxies) and to "did we accidentally train on the applicant's name or the answer key" (it refuses). A user-declared protected set is trusted but verified: an undeclared column that looks protected (e.g. marital_status, veteran_status) is surfaced as a mismatch to confirm, never silently reclassified. Supports EU AI Act Art. 10 (data governance) and data minimisation.

Refuse-Fast

If a declared target column does not exist, or the dataset is entirely identifiers / answer keys (nothing legitimate to assess), the result carries refuse=True with a plain refuse_reason — the caller stops before producing a misleading verdict.

Functions

classify_column_roles

Classify every column's role and surface identity/PII & oracle leakage plus declared-vs-inferred mismatches. Shared by Pulse and the Navigator profiling step (one source of truth).

python
from vfairness import classify_column_roles

schema = classify_column_roles(
    df,
    declared_protected=['age', 'gender', 'race_ethnicity'],
    declared_prediction='invite_decision',
    declared_target='hired',          # optional oracle
)

print(schema['pii_leakage'])       # ['first_name', 'date_of_birth', ...]
print(schema['proxy_candidates'])  # ['zip_minority_majority', ...]
for m in schema['mismatches']:
    print(m['column'], '->', m['inferred'], '-', m['detail'])
if schema['refuse']:
    print('STOP:', schema['refuse_reason'])

Returns a dict: roles (list of ColumnRole: column / role / confidence / reason / category), plus buckets pii_leakage, oracle_columns, model_output, protected, proxy_candidates, job_relevant, unknown, the mismatches list, and the refuse / refuse_reason gate.

Group-Size Reliability Tiers

A single n ≥ 30 cut-off hides the difference between a 31-person group (read with caution) and a 9-person group (the 95% margin of error exceeds the metric). group_reliability() assigns one shared tier per group, used identically by Pulse, the Navigator, and the Modules so every per-group number carries the same honesty flag.

TierSizeTreatment
reliablen ≥ 100Report normally.
caution30–99Report, note the wider confidence interval.
underpowered10–29Report greyed, "small sample — indicative only".
invalidn < 10Do not interpret the rate — shown, never ranked, excluded from the verdict.

group_reliability(sizes) returns {group: {n, tier, interpretable, note}}; reliability_tier(n) is the scalar form. Thresholds follow the Fairlearn small-group convention and the Turing M3 minimum-reporting guidance.

Selection-Rate Disparity

selection_rate_disparity_matrix() reports, per group, the selection rate with a 95% confidence interval (forest-plot ready), n, and reliability tier; plus the full group×group ratio and difference matrices and the worst pair.

Jurisdiction-neutral by design. This is pure descriptive statistics, valid everywhere. It deliberately does not apply the US EEOC four-fifths (0.80) rule or any legal bright-line. The four-fifths rule is US-employment-only and is not law in the EU / UK / Switzerland, which use a proportionality + objective-justification test with no fixed numeric bar. The legal reading is a separate, jurisdiction-aware overlay applied by the verdict engine — never baked into the metric.

Functions

selection_rate_disparity_matrix

Per-group rate + CI + tier and the group×group ratio/difference grid. Shared by Pulse and the Navigator metrics handler.

python
from vfairness import selection_rate_disparity_matrix

m = selection_rate_disparity_matrix(y_pred, race)
print(m['min_ratio'], m['min_ratio_pair'])   # worst adverse-impact pair
print(m['rates']['Black'])  # {rate, ci_low, ci_high, n, tier, interpretable}
print(m['ratio_matrix']['Black']['White'])   # rate(Black)/rate(White)

Returns groups, rates, ratio_matrix, difference_matrix, reference_group, min_ratio / min_ratio_pair, max_difference / max_difference_pair. The worst pair is computed over interpretable groups only (n≥10) so a tiny group's noisy rate never defines the headline.

Assurance Verdict

build_assurance_verdict() turns the computed analysis sections into one structured assurance opinion, following the assurance-audit taxonomy (Lam et al., FAccT 2024).

OpinionMeaning
UnqualifiedNo material fairness defect on the assessed attributes.
QualifiedDeployable only with the documented remediations + monitoring.
AdverseMaterial defects — unfit to deploy as-is (blocks deployment).
DisclaimerCannot form an opinion (refuse-fast, or insufficient data).
The jurisdiction-aware legal overlay lives here, not in the metrics. The US EEOC four-fifths 0.80 line is applied only for US employment; EU / UK / CH use proportionality + objective justification with no numeric bar. Emits stable-ID findings (with panelRef), priority-ranked recommendations (each routed to a Navigator step + a vfairnessFunction), metricsDeferred, jurisdictionBasis, and an auditTrail.

Functions

build_assurance_verdict

Assemble the structured assurance opinion from the analysis sections. Pure rules; never raises. Shared by the Pulse banner and the Navigator pre-fill (one source of truth).

python
from vfairness import build_assurance_verdict

v = build_assurance_verdict(
    schema=schema, per_variable=per_variable, metrics=metrics,
    bias=bias, proxies=proxies, statistical=statistical,
    intersectional=intersectional, disparity_matrix=dm,
    domain='hiring', jurisdiction='US', has_truth=True)

print(v['overall'], v['blocksDeployment'])   # e.g. "Adverse" True
print(v['oneLineVerdict'])
for f in v['findings']:        print(f['id'], f['severity'])
for r in v['recommendations']: print(r['id'], '->', r['routeTo'])

Returns overall, blocksDeployment, oneLineVerdict, findings[], recommendations[], metricsDeferred, jurisdictionBasis, auditTrail.

Advanced Inference & Generative Triage

Audit-grade statistical inference beyond bootstrap, plus the generative (prompts+outputs) routing — all reuse-first, no external infrastructure.

FunctionWhat it does
empirical_likelihood_ciDistribution-free CI for a proportion (EL ratio = binomial LR, chi-squared limiting; no scipy).
simultaneous_disparity_boundsBonferroni-exact EL bounds across every subgroup at once (Cherian & Candès JMLR 2024 framing) + worst-case ratio/difference bound. Shown in the Pulse disparity grid.
sequential_fairness_testAnytime-valid Wald SPRT (the lib's own SPRT, decoupled for two arrays) — stop as soon as evidence is (in)sufficient, no alpha-spending.
detect_specification_biasConstruct-validity / specification bias (Jacobs & Wallach 2021): target leakage, proxy target (e.g. cost↦health need, Obermeyer 2019), circular evaluation.
Generative routing. run_pulse auto-detects a prompts+outputs artefact and routes to a generative path that reuses OutputAnalyzer.analyze_all (12 metrics, Benjamini-Hochberg corrected, no API call) per protected attribute, mapped into the same build_assurance_verdict shape so the Pulse UX is unchanged. The tabular path is the default and misroute-safe.

Multimodal & generative routing (Phase 4(2)-B)

run_pulse auto-detects the artefact kind and routes to the matching reuse path; the tabular path is the misroute-safe default. Every path maps into the same assurance verdict so the UX is unchanged.

PathReuses / behaviour
llm_probe_pulse (B1)Live endpoint → CounterfactualTester via LLMApiProxy. No endpoint ⇒ honest Disclaimer, never a fabricated verdict.
agent_probe_pulse (B4)Agent traces → ToolBiasAuditor + ActionBiasAnalyzer across demographic groups.
vision_probe_pulse (B2)vfairness.vision skew / NDKL / bias-amplification on per-image demographic labels. FairFace/CLIP classification is sidecar-gated (VFAIRNESS_VISION_SIDECAR); no sidecar ⇒ available:false + metadata-only, never guessed demographics.
3-axis Pareto (B3)mitigation_pareto adds contextDistortion + a 3D frontier + the context-blind-parity warning (Gemini Feb 2024).

Live Progress Telemetry

run_pulse(df, inputs, progress=None) accepts an optional callback so callers can surface true per-stage progress instead of a time-based guess. Signature: progress(stage, label, stage_index, total_stages, pct).

Safe by construction. The parameter defaults to None and is invoked only through an internal helper that swallows every exception, so progress reporting can never affect the analysis or raise — a run with no callback is byte-identical to before. run_pulse has a single production caller (the Pulse consumer handler); the Navigator calls the underlying functions directly, so this is an additive, single-caller change.

The orchestrator emits 8 stages (total_stages = 8). Stage 0 is reported by the consumer handler before run_pulse; stages 1–7 by run_pulse itself, in execution order, immediately before each major block. Stage indices are monotonically increasing, so a consumer can drive a live phase indicator directly off stage_index / total_stages.

#Stage labelEmitted before
0Decrypting and loading your datasetconsumer handler
1Checking data quality and column rolesclassify_column_roles / quality
2Computing fairness metrics across every protected attributeper-variable + metric cards
3Detecting bias patterns and group disparityBiasDetector.full_audit
4Screening for proxy and redundant-encoding leakageproxy / leakage screen
5Running statistical robustness and confidence intervalsstatistical battery
6Mapping causal structure and intersectional subgroupscausal skeleton + intersectional
7Composing the assurance verdict and recommendationsbuild_assurance_verdict

Early-return routing paths (generative / LLM / agent / vision / refuse) legitimately emit fewer stages; the consumer still reports a final completion ping. The platform's live phase indicator (PulseDispatchView) mirrors this list 1:1 and follows the real stage_index when present, only falling back to a smooth time model for the gaps between pings — it never fabricates a stage count.

Proxy Variable Detection

Identifies features that correlate with protected attributes and could enable indirect discrimination, even when protected attributes are excluded from the model.

How Proxy Detection Works

The proxy detection pipeline processes every candidate feature in the dataset through a multi-stage filter. For each (feature, protected_attribute) pair, the system:

  1. Filters high-cardinality columns — any column with more than max_cardinality unique values or a unique-to-rows ratio > 0.5 is skipped entirely. This prevents ID-like columns (e.g. applicant_id) from producing spurious perfect correlations in sparse contingency tables.
  2. Encodes for correlation — categorical columns are label-encoded to numeric codes so that all four correlation measures can be computed.
  3. Computes four correlation measures — Pearson r, Spearman ρ, bias-corrected Cramér's V (Bergsma 2013), and optionally Mutual Information (MI).
  4. Selects the primary measure — the measure with the highest absolute value is used as the representative correlation strength.
  5. Applies the correlation threshold gate — if the primary correlation < correlation_threshold (default 0.3), the pair is discarded.
  6. Applies the p-value gate — if the p-value ≥ significance_level (default 0.05), the pair is discarded. For Cramér's V, the chi-squared p-value is used.
  7. Assigns a risk level — based on correlation magnitude and known proxy pattern matching (see thresholds table below).
  8. Classifies the proxy typeDIRECT, HISTORICAL, INDIRECT, or INTERSECTIONAL based on keyword analysis.
  9. Generates recommendations — tailored remediation advice based on the risk level.

Proxy Detection Decision Tree

The diagram below shows the full decision flow for each (feature, protected_attribute) pair. Click the diagram to zoom & pan.

Click diagram to zoom & pan
flowchart TD START(["identify_proxy_variables()"]) START --> LOOP["For each feature × protected_attribute pair"] LOOP --> CARD{"Cardinality
check"} CARD -->|"unique > max_cardinality
OR unique/rows > 0.5"| SKIP["⛔ Skip column
(e.g. applicant_id)"] CARD -->|"Pass"| ENCODE["Label-encode
categorical → numeric"] ENCODE --> CORR["Compute 4 measures:
Pearson r · Spearman ρ
Cramér's V (corrected) · MI"] CORR --> BEST{"Select highest
|correlation|"} BEST --> THR{"correlation
≥ threshold?"} THR -->|"No"| DROP1["⛔ Discard pair"] THR -->|"Yes"| PVAL{"p-value
< significance_level?"} PVAL -->|"No (not significant)"| DROP2["⛔ Discard pair"] PVAL -->|"Yes"| PATTERN{"Known proxy
pattern match?"} PATTERN -->|"high_risk keyword
+ corr ≥ 0.3"| CRIT["🚨 CRITICAL"] PATTERN -->|"No match or
medium_risk keyword"| RISK{"Correlation-based
risk level"} RISK -->|"≥ 0.7"| CRIT2["🚨 CRITICAL"] RISK -->|"0.5 – 0.7"| HIGH["🔴 HIGH"] RISK -->|"0.3 – 0.5"| MED["🟡 MEDIUM"] RISK -->|"0.1 – 0.3"| LOW["🟢 LOW"] RISK -->|"< 0.1"| NEG["⚪ NEGLIGIBLE"] CRIT --> TYPE CRIT2 --> TYPE HIGH --> TYPE MED --> TYPE LOW --> TYPE NEG --> TYPE TYPE{"Classify
proxy type"} TYPE -->|"zip, census,
school, credit"| HIST["ProxyType.HISTORICAL"] TYPE -->|"name, surname,
title, salutation"| DIRECT["ProxyType.DIRECT"] TYPE -->|"other"| DEFAULT["ProxyType.DIRECT
(default)"] HIST --> RESULT DIRECT --> RESULT DEFAULT --> RESULT RESULT(["ProxyVariableResult
+ recommendations"]) style START fill:#e0e7ff,stroke:#6366f1,stroke-width:2px,color:#1e1b4b style RESULT fill:#e0e7ff,stroke:#6366f1,stroke-width:2px,color:#1e1b4b style SKIP fill:#fef2f2,stroke:#ef4444,color:#7f1d1d style DROP1 fill:#fef2f2,stroke:#ef4444,color:#7f1d1d style DROP2 fill:#fef2f2,stroke:#ef4444,color:#7f1d1d style ENCODE fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f style CORR fill:#dbeafe,stroke:#3b82f6,color:#1e3a5f style CRIT fill:#fef2f2,stroke:#ef4444,stroke-width:2px,color:#7f1d1d style CRIT2 fill:#fef2f2,stroke:#ef4444,stroke-width:2px,color:#7f1d1d style HIGH fill:#fff7ed,stroke:#f97316,color:#7c2d12 style MED fill:#fef9c3,stroke:#eab308,color:#713f12 style LOW fill:#f0fdf4,stroke:#22c55e,color:#14532d style NEG fill:#f8fafc,stroke:#94a3b8,color:#475569 style HIST fill:#ede9fe,stroke:#8b5cf6,color:#3b0764 style DIRECT fill:#ede9fe,stroke:#8b5cf6,color:#3b0764 style DEFAULT fill:#ede9fe,stroke:#8b5cf6,color:#3b0764

Correlation Measures

Four measures are computed for every (feature, protected_attribute) pair. The one with the highest absolute value is used as the primary correlation strength.

Measure Range When used P-value source
Pearson r −1 … +1 Linear association between numeric (or encoded) columns. Computed on label-encoded values. scipy.stats.pearsonr
Spearman ρ −1 … +1 Monotonic (rank-order) association. More robust to non-linearity than Pearson. scipy.stats.spearmanr
Cramér's V 0 … 1 Association between two categorical (or categorised) variables. Uses the Bergsma 2013 bias-corrected formula to avoid inflation in sparse contingency tables. Chi-squared test (scipy.stats.chi2_contingency)
Mutual Information 0 … ∞ Non-linear dependency. Captures any kind of statistical relationship. Enabled when include_mutual_information=True. Not applicable (no parametric test)

Bias-Corrected Cramér's V (Bergsma 2013)

The naïve Cramér's V is known to be positively biased in sparse contingency tables — it inflates toward 1.0 when one or both variables have many categories relative to sample size. vfairness applies the Bergsma (2013) bias correction:

formula
ϕ² = χ² / n
ϕ²_corr = max(0,  ϕ² − (r−1)(k−1) / (n−1))
r̃ = r − (r−1)² / (n−1)
k̃ = k − (k−1)² / (n−1)
V_corr = √( ϕ²_corr / (min(r̃, k̃) − 1) )

where  r = rows,  k = columns,  n = sample size

Risk Level Thresholds

Risk is assigned in two stages: first by known-pattern matching, then by correlation magnitude.

Risk Level Correlation Range Condition
CRITICAL ≥ 0.7 Correlation ≥ 0.7 or known high_risk proxy pattern match with correlation ≥ 0.3
HIGH 0.5 – 0.7 Strong proxy signal — likely encodes protected attribute information
MEDIUM 0.3 – 0.5 Moderate signal — warrants investigation and domain expert review
LOW 0.1 – 0.3 Weak signal — monitor but unlikely to drive significant bias alone
NEGLIGIBLE < 0.1 No meaningful association detected

Known Proxy Patterns

The library ships with a dictionary of column-name keywords that are known proxies for specific protected attributes, drawn from fairness research. When include_known_patterns=True, any feature whose name contains a high_risk keyword and whose correlation ≥ 0.3 is immediately escalated to CRITICAL.

Protected Attribute High-Risk Keywords Medium-Risk Keywords Affected Groups
Race / Ethnicity zip, zipcode, postal, postcode, neighborhood, census_tract, surname, last_name, name school, college, university, language, address, city, district Black / African American, Hispanic / Latino, Asian, other minorities
Gender first_name, name, given_name, title, salutation, height, weight occupation, job_title, industry, major, field_of_study, hobbies Women, non-binary individuals
Age graduation_year, years_experience, tenure, birth_year technology_proficiency, digital_skills, social_media_usage, seniority Older workers (40+), young workers
Income zip, neighborhood, address, credit_score, credit_rating education_level, degree, school, car_make, device_type Low-income individuals, working class
Disability accommodation_request, ada_flag, accessibility, medical_leave typing_speed, response_time, interaction_time, error_rate People with disabilities
Religion name, surname, holiday_preference, time_off_requests dietary_preference, diet, neighborhood, community Religious minorities
National Origin name, surname, accent, language_proficiency, country_code education_country, degree_country, work_authorization, visa_status Immigrants, foreign nationals

Proxy Chain Detection

The find_proxy_chains() function detects 2-hop indirect proxy paths: Feature A → Feature B → Protected Attribute, where neither link alone exceeds the threshold but the chain reveals an indirect dependency.

Chain correlation is estimated as the product of the two individual correlations: chain_corr = corr(A,B) × corr(B,Protected). Only chains where chain_corr ≥ threshold are reported.

Caveat — correlation multiplication is a heuristic
Multiplying correlations does not account for confounders, partial correlations, or causal structure. Treat chain findings as hypotheses requiring domain expert review. For causal claims, use proper causal inference tools (DoWhy, CausalML) with directed acyclic graphs (DAGs).

ProxyVariableResult Fields

Each result from identify_proxy_variables() is a ProxyVariableResult dataclass with the following fields:

Field Type Description
featurestrColumn name identified as a potential proxy.
protected_attributestrProtected attribute it correlates with.
correlationfloatPrimary correlation coefficient (highest of the four measures).
correlation_typestrWhich measure produced the primary value ("Pearson", "Cramér's V", etc.).
risk_levelProxyRiskLevelEnum: CRITICAL, HIGH, MEDIUM, LOW, NEGLIGIBLE.
proxy_typeProxyTypeEnum: DIRECT, INDIRECT, HISTORICAL, INTERSECTIONAL.
mutual_informationOptional[float]MI score (if include_mutual_information=True).
cramers_vOptional[float]Bias-corrected Cramér's V (if computed).
pvalueOptional[float]Statistical significance of the primary correlation.
sample_sizeintNumber of rows used for computation.
affected_groupsList[str]Demographic groups potentially affected.
recommendationsList[str]Suggested remediation actions.
evidenceDict[str, Any]All correlation values, pattern match details, etc.

Systemic (Multivariate) Proxy Leakage

identify_proxy_variables() asks "does one feature leak a protected attribute?". multivariate_proxy_leakage() asks the harder, more decision-relevant question: "if I deleted the protected column entirely, could the model still rebuild it from everything else combined?" This is the redundant-encoding problem (Barocas & Selbst 2016; Datta et al. 2017): features that are each individually weak proxies can, together, reconstruct a protected attribute — so removing columns one by one never makes the model fair. It is the only proxy check that survives "we dropped the sensitive column."

Reading the score (for compliance / business)
A classifier is trained to guess the protected attribute using only the "allowed" columns. The held-out AUC runs 0.5 (no leakage) to 1.0 (fully recoverable). It is the evidence-grade answer to "but we don't even collect race", and supports EU AI Act Art. 10 (data governance) and disparate-impact analysis.
AUCReadingBusiness implication
~0.50No leakageRemoving the protected column is genuinely sufficient.
0.55–0.70Partial leakageResidual risk; document and monitor.
≥ 0.70Systemic leakageAttribute is redundantly encoded. Deleting columns will not de-bias the system — transform features or re-derive the label. Treat as a deployment blocker pending remediation.
≥ 0.85SevereThe protected attribute is almost fully recoverable from "neutral" data.

A cross-validated gradient-boosting classifier is trained on all non-protected, non-excluded columns to predict each protected attribute. Categorical predictors are one-hot encoded (so individual proxy categories — an HBCU tier, a minority-majority ZIP — are learnable). Held-out ROC AUC is reported both as a macro average over groups and as the worst (most-identifiable) single group, because one highly-recoverable subgroup is itself a leakage risk. The test is bounded for speed (row cap, adaptive CV folds) and degrades gracefully — too little data, one class, or a missing scikit-learn returns an explained "negligible/skipped" result, never an error.

MultivariateProxyResult Fields

FieldTypeDescription
protected_attributestrAttribute tested for reconstruction.
aucfloatHeadline reconstructability (max of macro and worst-group).
macro_aucfloatAverage one-vs-rest AUC across all groups.
worst_group_aucfloatAUC for the single most-identifiable group.
chance_aucfloat0.5 baseline reference.
severitystrnegligible | low | medium | high | severe.
systemic_leakageboolTrue when auc ≥ 0.70.
n_features_usedintPredictor columns used.
n_samples / n_classesintRows used; distinct attribute values.
top_contributorsList[Dict]Features ranked by mutual information (one-hot columns mapped back to source feature).
interpretationstrOne plain-language sentence for the report.

Functions

multivariate_proxy_leakage

Systemic-leakage test: can each protected attribute be reconstructed from all the non-protected features together? Shared by Pulse, the Navigator vfairness_proxy_analysis handler, and the Bias-Detection module (one source of truth).

python
from vfairness import multivariate_proxy_leakage

results = multivariate_proxy_leakage(
    df,
    protected_attributes=['race_ethnicity', 'gender', 'age'],
    exclude_columns=['applicant_id', 'invite_decision', 'true_label'],
    max_rows=5000,   # speed cap; works on any dataset size
    cv=5,
)

for r in results:                       # sorted by AUC, worst first
    d = r.to_dict()
    print(f"{d['protected_attribute']}: AUC {d['auc']:.2f} "
          f"({d['severity']}, systemic={d['systemic_leakage']})")
    print(f"  driven by: {[c['feature'] for c in d['top_contributors'][:5]]}")

Returns List[MultivariateProxyResult], sorted by AUC descending. Quantifies predictability, not causation — pair with identify_proxy_features (which feature), find_proxy_chains (the path), and the causal panel for the mechanism.

identify_proxy_variables

Detect features correlated with protected attributes using multiple correlation measures.

python
from vfairness.preprocessing.bias_detection import identify_proxy_variables

results = identify_proxy_variables(
    df,
    protected_attributes=['race', 'gender', 'age'],
    correlation_threshold=0.3,
    include_mutual_information=True,
    include_known_patterns=True,
    max_cardinality=50,
    significance_level=0.05,
)
Parameters
df pd.DataFrame Required
Input dataset containing features and protected attributes.
protected_attributes List[str] Required
Column names of protected attributes to check for proxy correlations.
correlation_threshold float Optional 0.3
Minimum correlation strength to flag a feature as a potential proxy.
include_mutual_information bool Optional True
Compute mutual information scores in addition to correlation measures.
include_known_patterns bool Optional True
Check against known proxy relationships (e.g., ZIP code → race, name → gender).
max_cardinality int Optional 50
Maximum number of unique values a column may have to be tested. Columns exceeding this (or with a unique-to-rows ratio > 0.5) are skipped to prevent false positives from ID-like columns.
significance_level float Optional 0.05
Maximum p-value for a correlation to be considered statistically significant. Pairs with p-value ≥ this threshold are discarded.
Returns

List[ProxyVariableResult] — each result includes correlation strength, type (Pearson, Cramér's V, MI), risk level, affected groups, and recommendations.

compute_proxy_correlations

Compute all correlation measures between a specific feature and a protected attribute.

python
compute_proxy_correlations(
    df: pd.DataFrame,
    feature: str,
    protected_attribute: str
) -> Dict[str, Any]
Parameters
df pd.DataFrame Required
DataFrame containing both columns.
feature str Required
Column name of the candidate proxy feature.
protected_attribute str Required
Column name of the protected attribute to test correlation against.
Returns

Dict[str, Any] — Contains primary_correlation, cramers_v, mutual_information, risk_level, is_known_pattern, and correlation type used.

Fairness-Aware Feature Transforms

Feature Engineering Module

The Feature Engineering module provides tools for creating fair feature representations by identifying and addressing proxy variables and discriminatory features.

Demo Notebook: vfairness_1_feature_engineering_demo.ipynb — Proxy detection, correlation analysis & feature transforms

Fair Feature Engineering

Learn how to identify proxy variables and transform features for fairer ML models

0:00 / 0:00
Click diagram to zoom & pan
flowchart TB
    subgraph FE["Feature Engineering Module"]
        FEA["FeatureEngineeringAnalyzer
(Unified Interface)"] subgraph Analysis["Analysis Components"] PC["Proxy Detection"] CA["Correlation Analysis"] IC["Intersectional Analysis"] end subgraph Transform["Transformation Components"] CR["CorrelationReducer"] FS["FeatureSuppressor"] RT["ResidualTransformer"] IT["IntersectionalTransformer"] RW["ReweightingTransformer"] DIR["DisparateImpactRemover"] LM["LabelMassager"] RS["Resampler"] FRT["FairRepresentationTransformer"] end subgraph Viz["Visualization"] HM["Correlation Heatmaps"] RC["Risk Charts"] TC["Transformation Comparison"] end end FEA --> Analysis FEA --> Transform FEA --> Viz style FEA fill:#4A90D9,stroke:#333,stroke-width:2px,color:#fff style Analysis fill:#f0f9ff,stroke:#0ea5e9 style Transform fill:#f0fdf4,stroke:#22c55e style Viz fill:#fefce8,stroke:#eab308

Key Concepts

Proxy Variables
Features that indirectly encode protected attributes (e.g., ZIP code as proxy for race)
Correlation Reduction
Techniques to reduce discriminatory signals while preserving predictive power
Fair Representation
Transformed features that promote fairness across demographic groups
Intersectional Analysis
Addressing compound effects of multiple protected attributes
Disparate Impact Removal
Aligns each numeric feature's distribution across protected groups (Feldman et al. 2015) to remove disparate impact while preserving within-group ranking. Tunable repair_level. With multiple protected attributes it repairs across their intersection (e.g. race x gender).
Label Massaging
Relabels borderline instances near the decision boundary (Kamiran & Calders 2012) to equalize group positive rates, correcting historical bias in the training labels. Exposes get_massaged_labels(); safety-capped by max_flip_fraction.
Resampling
Balances group (and group x label) representation by random over-/under-sampling (Kamiran & Calders 2012). Exposes get_resampled_data(); configurable strategy and balance_by. Multi-attribute selections balance intersectional cells.
Fair Representation
Learns a latent feature code that preserves task signal but is invariant to protected attributes, via an autoencoder with a gradient-reversal adversary (Zemel et al. 2013 LFR; Louizos et al. 2016 VFAE; Madras et al. 2018 LAFTR). transform returns columns rep_0..; handles multiple attributes via their intersection. Requires torch.

FeatureEngineeringAnalyzer

The unified interface for fairness-aware feature engineering analysis and transformation.

Background

Features that correlate with protected attributes can introduce bias even when those attributes are excluded from training. FeatureEngineeringAnalyzer provides an end-to-end workflow: it identifies fairness-relevant correlations using mixed-measure analysis (Pearson, point-biserial, Cramér's V), quantifies proxy-variable risk, and transforms features to reduce unfair associations via CorrelationReducer and ResidualTransformer.

The analyzer auto-selects the appropriate correlation measure per feature pair (see the Feature Correlation Visualization section), supports intersectional analysis across multiple protected attributes, and produces structured reports suitable for documentation or compliance review.

class FeatureEngineeringAnalyzer Main Class

Unified interface for fairness-aware feature engineering — proxy identification, correlation analysis, feature transformation, and actionable recommendations.

Constructor

python
FeatureEngineeringAnalyzer(
    df: pd.DataFrame,
    protected_attributes: List[str],
    *,
    target_column: Optional[str] = None,
    feature_columns: Optional[List[str]] = None,
    correlation_threshold: float = 0.3,
    min_sample_size: int = 100
)
Parameters
df pd.DataFrame Required
DataFrame containing features and protected attributes.
protected_attributes List[str] Required
List of column names for protected attributes (e.g., ['gender', 'race']).
target_column Optional[str] Optional None
Target/outcome column for supervised analysis (keyword-only).
feature_columns Optional[List[str]] Optional None
Specific feature columns to analyze. If None, all numeric non-protected, non-target columns are auto-detected.
correlation_threshold float Optional 0.3
Threshold for flagging feature–attribute correlations as noteworthy.
min_sample_size int Optional 100
Minimum number of samples required for reliable analysis.

Key Methods

Method Description
full_analysis() Comprehensive analysis including proxies, correlations, and recommendations
analyze_proxies() Identify proxy variables with risk assessment
get_correlation_matrix() Compute feature-attribute correlation matrix
transform(method=...) Apply fairness transformation to features
compare_transformations() Compare multiple transformation methods
get_feature_recommendations() Get prioritized recommendations for each feature
python
from vfairness.preprocessing.feature_engineering import FeatureEngineeringAnalyzer

# Create analyzer
analyzer = FeatureEngineeringAnalyzer(
    df=loan_data,
    protected_attributes=['race', 'gender'],
    target_column='approved'
)

# Run comprehensive analysis
report = analyzer.full_analysis()
print(report.summary)

# View risk breakdown
print(f"Critical: {report.risk_summary['critical']}")
print(f"High: {report.risk_summary['high']}")

# Get recommendations
for rec in analyzer.get_feature_recommendations(top_n=5):
    print(f"[{rec['priority']}] {rec['feature']}: {rec['action']}")

# Transform features
X_fair = analyzer.transform(method='correlation_reduction')

# Compare different methods
comparison = analyzer.compare_transformations()
print(comparison)

Correlation Analysis

Tools for analyzing correlations between features and protected attributes.

compute_feature_correlations

Compute correlation matrix between all features and protected attributes using appropriate measures based on data types. Automatically selects Pearson, point-biserial, or Cramér's V depending on the variable types.

python
compute_feature_correlations(
    df: pd.DataFrame,
    protected_attributes: List[str],
    *,
    feature_columns: Optional[List[str]] = None,
    method: str = 'auto',
    include_pvalues: bool = True
) -> FeatureCorrelationMatrix
Parameters
df pd.DataFrame Required
DataFrame to analyze.
protected_attributes List[str] Required
List of protected attribute column names.
feature_columns Optional[List[str]] Optional None
Columns to include as features. If None, all non-protected columns are auto-detected (keyword-only).
method str Optional 'auto'
Correlation method: 'auto' (selects per type pair), 'pearson', 'spearman', or 'cramers_v'.
include_pvalues bool Optional True
Whether to compute statistical significance p-values for each correlation.
Returns

FeatureCorrelationMatrix — dataclass containing: correlations (DataFrame of correlation values), pvalues (DataFrame of p-values), methods_used (Dict of method per pair), and helper get_high_correlations(threshold) to retrieve features above a given threshold.

python
from vfairness.preprocessing.feature_engineering import compute_feature_correlations

# Compute correlations
matrix = compute_feature_correlations(
    df=data,
    protected_attributes=['gender', 'race'],
    method='auto'  # Auto-selects Pearson, Cramér's V, or point-biserial
)

# View correlation matrix
print(matrix.correlations)

# Get high correlations
high_corr = matrix.get_high_correlations(threshold=0.3)
for feature, attr, corr in high_corr:
    print(f"{feature} ↔ {attr}: {corr:.3f}")

Proxy Detection

Identify features that serve as proxies for protected attributes.

identify_proxy_variables

Detect features that could enable indirect discrimination. Results are sorted by risk level (critical first), then by absolute correlation.

python
identify_proxy_variables(
    df: pd.DataFrame,
    protected_attributes: List[str],
    *,
    feature_columns: Optional[List[str]] = None,
    correlation_threshold: float = 0.3,
    include_mutual_information: bool = True,
    include_known_patterns: bool = True,
    min_sample_size: int = 100,
    max_cardinality: int = 50,
    significance_level: float = 0.05
) -> List[ProxyVariableResult]
Parameters
df pd.DataFrame Required
DataFrame to analyze for proxy variables.
protected_attributes List[str] Required
List of protected attribute column names.
feature_columns Optional[List[str]] Optional None
Columns to test as potential proxies. If None, all non-protected columns are auto-detected (keyword-only).
correlation_threshold float Optional 0.3
Minimum correlation value to flag a feature as a proxy.
include_mutual_information bool Optional True
Whether to compute mutual information in addition to correlation.
include_known_patterns bool Optional True
Whether to check features against built-in known proxy patterns (e.g., zip code → race).
min_sample_size int Optional 100
Minimum sample size for reliable proxy computation.
max_cardinality int Optional 50
Maximum number of unique values for a feature column to be included in analysis.
significance_level float Optional 0.05
P-value threshold for statistical significance of correlations.
Returns

List[ProxyVariableResult] — sorted by risk level (critical first). Each result contains: feature, protected_attribute, correlation, risk_level (CRITICAL / HIGH / MEDIUM / LOW / NEGLIGIBLE), proxy_type (DIRECT / INDIRECT / INTERSECTIONAL / HISTORICAL), mutual_information, p_value, and recommendations.

Risk Levels

Level Correlation Action
CRITICAL ≥ 0.7 Remove or apply strict constraints
HIGH 0.5 - 0.7 Consider removal or transformation
MEDIUM 0.3 - 0.5 Monitor for disparate impact
LOW 0.1 - 0.3 Continue monitoring

Known Proxy Patterns

The module includes built-in knowledge of common proxy patterns:

python
from vfairness.preprocessing.feature_engineering import KNOWN_PROXY_PATTERNS

# Race proxies: zip code, neighborhood, surname, school name
# Gender proxies: first name, title, occupation, field of study
# Age proxies: graduation year, years experience, birth year
# Income proxies: zip code, credit score, education level
python
from vfairness.preprocessing.feature_engineering import identify_proxy_variables

# Identify proxies
proxies = identify_proxy_variables(
    df=data,
    protected_attributes=['race', 'gender'],
    correlation_threshold=0.3
)

for proxy in proxies:
    print(f"\n{proxy.feature}:")
    print(f"  Risk: {proxy.risk_level.value}")
    print(f"  Correlation: {proxy.correlation:.3f} with {proxy.protected_attribute}")
    print(f"  Type: {proxy.proxy_type.value}")
    for rec in proxy.recommendations:
        print(f"  → {rec}")

Proxy Chains

Find indirect proxy relationships (A → B → Protected Attribute):

python
from vfairness.preprocessing.feature_engineering import find_proxy_chains

chains = find_proxy_chains(df, 'race', correlation_threshold=0.3)

for chain in chains:
    print(f"Chain: {' → '.join(chain['chain'])}")
    print(f"  Indirect correlation: {chain['indirect_correlation']:.3f}")

Feature Transformers

Scikit-learn compatible transformers for fairness-aware feature engineering.

Transformer Method Description
CorrelationReducer residualize, decorrelate, partial Reduces correlation with protected attributes
FeatureSuppressor remove, mask, noise, bin Removes or masks discriminatory features
ResidualTransformer group_mean, regression, quantile Creates residualized features (Feldman et al.)
IntersectionalTransformer merge, keep, exclude Handles intersectional fairness
ReweightingTransformer inverse_frequency, target_parity Computes sample weights for balance

CorrelationReducer

python
from vfairness.preprocessing.feature_engineering import CorrelationReducer

# Create reducer
reducer = CorrelationReducer(
    protected_attributes=['gender', 'race'],
    method='residualize',  # 'residualize', 'decorrelate', 'partial'
    target_correlation=0.1,
    preserve_variance=True
)

# Fit and transform
X_fair = reducer.fit_transform(X)

# Check transformation result
print(f"Correlation before: {reducer.fit_result.correlation_before}")
print(f"Correlation after: {reducer.fit_result.correlation_after}")

FeatureSuppressor

python
from vfairness.preprocessing.feature_engineering import FeatureSuppressor

# Remove discriminatory features
suppressor = FeatureSuppressor(
    protected_attributes=['race'],
    strategy='remove',  # 'remove', 'mask', 'noise', 'bin'
    correlation_threshold=0.3,
    features_to_suppress=['zip_code', 'neighborhood']  # Optional: explicit list
)

X_fair = suppressor.fit_transform(X)
print(f"Features removed: {suppressor.fit_result.features_removed}")

Pipeline Integration

python
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from vfairness.preprocessing.feature_engineering import CorrelationReducer

# Create fairness-aware pipeline
pipeline = Pipeline([
    ('fair_transform', CorrelationReducer(
        protected_attributes=['gender'],
        method='residualize'
    )),
    ('scaler', StandardScaler()),
    ('classifier', LogisticRegression())
])

# Fit pipeline
pipeline.fit(X_train, y_train)
predictions = pipeline.predict(X_test)

Feature Engineering Visualization

Visual tools for understanding and communicating feature fairness analysis. The centrepiece is the mixed-measure correlation heatmap, which automatically selects the statistically appropriate association measure for each feature–attribute pair based on the variable types involved.

Correlation Heatmap
Mixed-measure heatmap — auto-selects Pearson / point-biserial / Cramér's V per pair
plot_correlation_heatmap()
Correlation Bars
Bar chart of correlations for a single protected attribute
plot_correlation_bars()
Correlation Matrix
Pearson-only correlation matrix across all numeric features
plot_feature_correlation_matrix()
Proxy Risk Chart
Proxy variables ranked by risk level
plot_proxy_risk_chart()
Risk Distribution
Donut chart showing distribution of risk levels
plot_risk_distribution()
Transformation Comparison
Before / after correlation comparison for transformations
plot_transformation_comparison()
Analysis Dashboard
Multi-panel dashboard combining all analysis views
create_analysis_dashboard()

How the Mixed-Measure Correlation Matrix Works

Real-world fairness datasets contain a mix of numeric, binary, and multi-class categorical variables. A single correlation statistic (e.g. Pearson r) cannot correctly capture the association between all type combinations. When method='auto' (the default), compute_feature_correlations() inspects each feature–attribute pair and selects the most appropriate measure:

Feature Type Attribute Type Measure Selected Range Interpretation
Numeric Numeric / ordinal Pearson r [−1, +1] Linear relationship strength and direction
Numeric Binary (2 levels) Point-biserial r [−1, +1] Equivalent to Pearson r between a continuous and a dichotomous variable
Categorical Categorical Cramér's V [0, 1] Symmetric association based on the chi-squared statistic, bias-corrected for small samples
Numeric Multi-class categorical Cramér's V [0, 1] The numeric feature is discretised (quantile binning) before computing Cramér's V
Why not use Pearson everywhere?

Pearson r assumes both variables are continuous and linearly related. Applying it to categorical data (e.g. zip_code × race) produces misleading near-zero values even when the two variables are strongly associated. Cramér's V and point-biserial r are designed for exactly these mixed-type scenarios, giving a faithful picture of how much information a feature carries about a protected attribute.

The heatmap annotates each cell with both the absolute correlation value and the measure used, so reviewers can immediately see which statistical test produced each number. Values at or above the default threshold of 0.30 are highlighted in warm colors and flag the feature as a potential proxy.

python
from vfairness.preprocessing.feature_engineering import (
    FeatureEngineeringAnalyzer,
    plot_correlation_heatmap,
    plot_feature_correlation_matrix,
    plot_proxy_risk_chart,
    create_analysis_dashboard
)
import matplotlib.pyplot as plt

# 1. Mixed-measure correlation heatmap (features × protected attributes)
#    Automatically picks Pearson, point-biserial, or Cramér's V per pair
analyzer = FeatureEngineeringAnalyzer(protected_attributes=['gender', 'race', 'age_group'])
analyzer.fit(df)

matrix = analyzer.get_correlation_matrix()   # method='auto' by default
ax = plot_correlation_heatmap(matrix, annotate=True,
                              title='Feature–Attribute Correlations')
plt.savefig('correlation_heatmap.png', dpi=300)

# 2. Pearson-only matrix (numeric features only — classic heatmap)
ax = plot_feature_correlation_matrix(
    df,
    feature_columns=['income', 'credit_score', 'years_employed', 'loan_amount'],
    annotate=True
)
plt.savefig('feature_correlation_matrix.png', dpi=300)

# 3. Proxy risk chart
proxies = analyzer.analyze_proxies()
ax = plot_proxy_risk_chart(proxies, max_features=15)
plt.savefig('proxy_risk_chart.png', dpi=300)

# 4. Comprehensive dashboard
fig = create_analysis_dashboard(
    correlation_matrix=matrix,
    proxy_variables=proxies
)
plt.savefig('feature_analysis_dashboard.png', dpi=300)

SVG Templates vfairness.rendering

Lightweight, dependency-free SVG alternatives generated via Jinja2 templates — ideal for CI/CD artifacts, HTML reports, and headless environments. Each function accepts an optional save_path and returns the SVG string (3–6 KB).

View all preprocessing templates in the SVG Gallery →

python
from vfairness.rendering import correlation_heatmap_to_svg, proxy_risk_to_svg

# SVG from correlation matrix — no Matplotlib needed
matrix = analyzer.get_correlation_matrix()
svg = correlation_heatmap_to_svg(matrix, save_path="corr_heatmap.svg")

# SVG from proxy analysis
proxies = analyzer.analyze_proxies()
svg = proxy_risk_to_svg(proxies, save_path="proxy_risk.svg")

2. Training-Time Interventions

Fairness-aware model training interventions applied during the learning process. The in_processing module provides comprehensive tools for incorporating fairness constraints directly into model training.

Demo Notebook: vfairness_2_training_demo.ipynb — Fair training with loss functions, constraints & regularizers

Fairness in Model Training

Overview of in-processing techniques: loss functions, constraints, regularizers, and scikit-learn wrappers

0:00 / 0:00
python
from vfairness.in_processing import (
    # Scikit-learn wrappers
    FairClassifier, FairRegressor,
    # Analyzer
    FairnessTrainingAnalyzer, FairnessTrainingReport,
    # Loss functions (PyTorch)
    DemographicParityLoss, EqualizedOddsLoss, EqualOpportunityLoss,
    AdversarialDebiasingLoss, CounterfactualFairnessLoss,
    # Constraints
    ExponentiatedGradient, GridSearch, ThresholdOptimizer,
    DemographicParityConstraint, EqualizedOddsConstraint,
    # Regularizers
    StatisticalParityRegularizer, HilbertSchmidtRegularizer,
    # Calibrators
    TrainableGroupCalibrator, CalibrationAwareTrainer,
)

Quick Start

python
# Scikit-learn compatible approach (easiest)
from sklearn.ensemble import RandomForestClassifier
from vfairness.in_processing import FairClassifier

clf = FairClassifier(
    base_estimator=RandomForestClassifier(),
    fairness_constraint='demographic_parity',
    tolerance=0.05
)
clf.fit(X_train, y_train, sensitive_attr=gender)
y_pred = clf.predict(X_test)
print(f"Constraint satisfied: {clf.fairness_result_.constraint_satisfied}")

Module Architecture

The in_processing module offers five main approaches:

Loss Functions
PyTorch losses that penalize fairness violations during training
Constraints
Algorithms that enforce hard fairness constraints (reductions approach)
Regularizers
Penalty terms added to any differentiable loss function
Calibrators
Trainable group-wise probability calibration
Scikit-Learn Wrappers
Easy-to-use wrappers for standard ML workflows
flowchart TB subgraph IP["In-Processing Module"] direction TB subgraph Wrappers["Scikit-Learn API"] FC["FairClassifier
sklearn-compatible wrapper"] end subgraph Core["Core Fairness Approaches"] direction LR LF["Loss Functions
DemographicParityLoss
EqualOpportunityLoss
EqualizedOddsLoss"] CB["Constraints
ExponentiatedGradient
reductions approach"] RG["Regularizers
DemographicParityReg
EqualOpportunityReg"] end subgraph Train["Training Loop"] direction LR FWD["Forward Pass"] --> LOSS["Task Loss
+ Fairness Penalty"] LOSS --> BWD["Backward Pass"] BWD --> UPD["Update Weights"] UPD --> FWD end end FC --> Core LF --> Train RG --> Train CB -.->|"iterative
reweighting"| Train style IP fill:#f0f9ff,stroke:#0aafe3,stroke-width:2px style Wrappers fill:#e0f2fe,stroke:#0aafe3 style Core fill:#f0f9ff,stroke:#0aafe3 style Train fill:#fff,stroke:#0aafe3 style FC fill:#0aafe3,color:#fff,stroke:#0c4a6e,stroke-width:2px style LF fill:#059669,color:#fff style CB fill:#059669,color:#fff style RG fill:#059669,color:#fff style LOSS fill:#fef3c7,stroke:#f59e0b,color:#92400e
Click diagram to zoom & pan

Choosing Your Approach: Reductions vs. Lagrangian Methods

The most important decision in fairness-aware training is selecting the right optimization strategy for your model type. This choice fundamentally affects how fairness constraints are enforced during learning.

flowchart TD START["What type of model
are you training?"] --> Q1{"Can you access
and modify the
training loop?"} Q1 -->|"No - Black-box model
(sklearn, XGBoost, etc.)"| REDUCTIONS["Use Reductions Approach
ExponentiatedGradient
GridSearch"] Q1 -->|"Yes - Custom training
(PyTorch, TensorFlow)"| Q2{"Do you need
guaranteed constraint
satisfaction?"} Q2 -->|"Yes - Hard constraints"| REDUCTIONS Q2 -->|"No - Soft penalties OK"| LAGRANGIAN["Use Lagrangian Methods
FairnessLosses
Regularizers"] REDUCTIONS --> R_RESULT["✅ Works with ANY classifier
✅ Theoretical guarantees
✅ No gradient access needed
⚠️ Slower (multiple retrains)"] LAGRANGIAN --> L_RESULT["✅ End-to-end differentiable
✅ Single training pass
✅ Fine-grained control
⚠️ Soft constraints only"] style START fill:#0aafe3,color:#fff,stroke:#0c4a6e,stroke-width:2px style Q1 fill:#fef3c7,stroke:#f59e0b,color:#92400e style Q2 fill:#fef3c7,stroke:#f59e0b,color:#92400e style REDUCTIONS fill:#059669,color:#fff,stroke:#0c4a6e style LAGRANGIAN fill:#6366f1,color:#fff,stroke:#4338ca style R_RESULT fill:#f0f9ff,stroke:#0aafe3,color:#0c4a6e style L_RESULT fill:#e0e7ff,stroke:#6366f1,color:#3730a3
Click diagram to zoom & pan

Reductions Approach

For black-box or non-differentiable models where you cannot modify the internal training loop. Treats the classifier as a black box and achieves fairness through iterative reweighting.

When to Use
  • Scikit-learn classifiers (RandomForest, SVM, etc.)
  • Gradient boosting (XGBoost, LightGBM, CatBoost)
  • Any model with fit/predict interface
  • When you need guaranteed constraint satisfaction
  • When model internals are inaccessible
Key Components
ExponentiatedGradient GridSearch ThresholdOptimizer

Lagrangian Methods

For deep learning models where you control the training loop. Adds fairness penalties directly to the loss function, enabling end-to-end gradient-based optimization.

When to Use
  • PyTorch or TensorFlow neural networks
  • Custom differentiable models
  • When you want single-pass training efficiency
  • When soft constraint violations are acceptable
  • When you need fine-grained fairness-accuracy control
Key Components
DemographicParityLoss EqualizedOddsLoss StatisticalParityRegularizer

Mathematical Background

The Core Problem: Constrained Optimization

Both approaches solve the same fundamental problem—training a model that minimizes prediction error subject to fairness constraints:

minimizeθ Ltask(θ)    subject to    g(θ) ≤ ε

Where Ltask is prediction loss, g(θ) measures fairness violation, and ε is the tolerance.

1 Reductions: Iterative Reweighting

The reductions approach (Agarwal et al., 2018) converts the constrained problem into a sequence of cost-sensitive classification problems:

  1. Initialize sample weights uniformly
  2. Train base classifier on weighted samples
  3. Evaluate constraint violations on predictions
  4. Update weights using exponentiated gradient
  5. Repeat until convergence or max iterations
λ(t+1) = λ(t) · exp(η · ∇g(θ(t)))

The multipliers λ exponentially increase for violated constraints, forcing subsequent classifiers to respect them.

2 Lagrangian: Penalty in Loss Function

Lagrangian methods add the constraint as a differentiable penalty to the loss, enabling gradient-based optimization:

Ltotal = Ltask + λ · Lfairness

Since standard fairness metrics (TPR, FPR) are non-differentiable, we use soft approximations:

Soft TPR: mean(ŷ | y=1) ≈ Σ(ŷ · y) / Σ(y)
Soft FPR: mean(ŷ | y=0) ≈ Σ(ŷ · (1-y)) / Σ(1-y)

Gradients flow through these soft metrics, allowing joint optimization of accuracy and fairness.

Detailed Comparison

Aspect Reductions (ExponentiatedGradient) Lagrangian (Loss Functions)
Model Requirements Any classifier with fit/predict Differentiable model (neural network)
Constraint Type Hard constraints with guarantees Soft penalties (may violate slightly)
Training Passes Multiple (one per iteration) Single pass
Computational Cost Higher (retrains base model N times) Lower (standard gradient descent)
Hyperparameters tolerance (ε), max_iterations lambda_fairness (λ), warmup_epochs
Convergence Theoretical guarantees (PAC learning) Depends on λ tuning
Best For Production ML pipelines, sklearn Deep learning, research

Implementation Tips

For Reductions
  • Start with tolerance=0.05 and adjust
  • Use GridSearch first for exploration
  • Monitor converged flag in results
  • The algorithm returns a mixture of classifiers
For Lagrangian
  • Start with λ=0.1 and use warmup
  • Monitor both task and fairness loss
  • If oscillating, reduce λ or increase warmup
  • Use FairnessTrainingAnalyzer for tuning

Quick Comparison: Same Task, Different Approaches

Reductions (sklearn)
# For RandomForest, XGBoost, etc.
from sklearn.ensemble import RandomForestClassifier
from vfairness.in_processing import (
    ExponentiatedGradient,
    DemographicParityConstraint
)

# Wrap any sklearn classifier
eg = ExponentiatedGradient(
    base_estimator=RandomForestClassifier(),
    constraint=DemographicParityConstraint(
        tolerance=0.05
    ),
    max_iterations=50
)

# Standard fit/predict interface
eg.fit(X_train, y_train,
       sensitive_attr=gender)
y_pred = eg.predict(X_test)

# Check if constraint satisfied
print(f"Converged: {eg.result_.converged}")
print(f"Violation: {eg.result_.final_violation}")
Lagrangian (PyTorch)
# For neural networks
import torch
from vfairness.in_processing import (
    DemographicParityLoss
)

# Add fairness to loss function
loss_fn = DemographicParityLoss(
    lambda_fairness=0.1,
    warmup_epochs=5
)

# Standard PyTorch training loop
for epoch in range(num_epochs):
    loss_fn.set_epoch(epoch)
    for x, y, sens in dataloader:
        optimizer.zero_grad()
        y_pred = model(x).sigmoid()

        # Single loss combines both
        loss = loss_fn(y_pred, y, sens)

        loss.backward()
        optimizer.step()

    metrics = loss_fn.end_epoch()
    print(f"Fairness: {metrics.fairness_loss}")
Key Insight: They Solve the Same Problem Differently

Both methods aim for the same goal—fair predictions. Reductions achieves this by repeatedly retraining with adjusted sample weights until constraints are met. Lagrangian achieves this by adding a penalty term that pulls the model toward fairness during each gradient step. Choose based on your model type, not the fairness criterion.

Scikit-Learn Wrappers

FairClassifier

Scikit-learn compatible wrapper that adds fairness constraints to any base classifier.

Why a Wrapper?

In practice, most ML teams already have well-tuned classifiers (Random Forests, Gradient Boosting, Logistic Regression). FairClassifier lets you keep your existing model and simply wrap it with a fairness layer — no need to rewrite your pipeline or switch to a different algorithm.

Think of it like a fairness adapter: your model learns to make predictions as usual, but the wrapper adjusts how it trains (by reweighting samples) or how it predicts (by adjusting thresholds per group) so that the final output satisfies a fairness constraint like "equal approval rates across groups."

Under the hood, it delegates to one of three strategies — reductions (Exponentiated Gradient), grid_search, or threshold (post-hoc) — but exposes a simple fit() / predict() API that slots directly into scikit-learn pipelines, cross-validation, and grid search.

python
from sklearn.ensemble import RandomForestClassifier
from vfairness.in_processing import FairClassifier

# Create fair classifier
clf = FairClassifier(
    base_estimator=RandomForestClassifier(n_estimators=100),
    fairness_constraint='equalized_odds',  # or 'demographic_parity', 'equal_opportunity'
    tolerance=0.05,                         # Maximum allowed constraint violation
    method='reductions',                    # 'reductions', 'threshold', 'grid_search'
    max_iterations=50,
    verbose=True
)

# Fit (requires sensitive_attr)
clf.fit(X_train, y_train, sensitive_attr=gender)

# Predict
y_pred = clf.predict(X_test)

# Check fairness results
print(f"Accuracy: {clf.fairness_result_.accuracy:.4f}")
print(f"Violation: {clf.fairness_result_.fairness_violation:.4f}")
print(f"Satisfied: {clf.fairness_result_.constraint_satisfied}")

Parameters

ParameterTypeDefaultDescription
base_estimatorAnyRequiredAny scikit-learn compatible classifier
fairness_constraintstr'demographic_parity'Constraint type
tolerancefloat0.05Maximum allowed violation
methodstr'reductions'Training method
max_iterationsint50Max iterations for optimization
verboseboolFalsePrint progress

Available Fairness Constraints

ConstraintDescription
'demographic_parity'Equal positive prediction rates across groups
'equalized_odds'Equal TPR and FPR across groups
'equal_opportunity'Equal TPR only across groups
'false_positive_parity'Equal FPR only across groups
'bounded_group_loss'Bounded worst-group loss (minimax fairness)

Training Methods

MethodDescriptionBest For
'reductions'Exponentiated gradient algorithm (Agarwal et al. 2018)General-purpose, strong guarantees
'threshold'Post-processing threshold optimizationPre-trained models
'grid_search'Grid search over Lagrange multipliersQuick exploration

FairRegressor

Scikit-learn compatible wrapper for fairness-aware regression.

When Fairness Matters in Regression

Regression models predict continuous values — salaries, credit scores, insurance premiums, risk ratings. Fairness in regression means that the average predicted value should not systematically differ between demographic groups (mean parity), or that the prediction error should be equally distributed (error parity).

For example, if a model predicts home loan amounts, mean parity ensures the average loan offer is similar across groups, while error parity ensures the model is not more accurate for one group than another. FairRegressor enforces these properties by adjusting sample weights during training so the model learns to treat groups equitably.

python
from sklearn.linear_model import Ridge
from vfairness.in_processing import FairRegressor

reg = FairRegressor(
    base_estimator=Ridge(),
    fairness_constraint='mean_parity',  # 'mean_parity', 'error_parity', 'bounded_loss'
    tolerance=0.1,
    method='reweighting'
)

reg.fit(X_train, y_train, sensitive_attr=group)
y_pred = reg.predict(X_test)

FairnessTrainingAnalyzer

Comprehensive analyzer for evaluating and comparing fairness-aware training approaches.

The Fairness-Accuracy Trade-off & Pareto Frontiers

Whenever you add a fairness constraint to a model, you are asking it to optimize for two objectives at once: accuracy and fairness. These often conflict — making a model perfectly fair may reduce its accuracy, and vice versa. The set of best possible compromises is called the Pareto frontier.

FairnessTrainingAnalyzer automates the process of exploring this frontier. It sweeps over different values of λ (the regularization weight), trains a model for each, records accuracy and fairness violation, and identifies which configurations are Pareto-optimal — meaning no other configuration is better on both accuracy and fairness simultaneously.

This is especially valuable for communicating results to stakeholders: the resulting Pareto plot makes it visually clear what accuracy cost is required for a given level of fairness, helping teams make an informed, transparent decision about where to operate.

python
from sklearn.linear_model import LogisticRegression
from vfairness.in_processing import FairnessTrainingAnalyzer

# Create analyzer
analyzer = FairnessTrainingAnalyzer(
    X=X_train, y=y_train, sensitive_attr=gender,
    fairness_constraint='demographic_parity',
    tolerance=0.05
)

# Full analysis
report = analyzer.full_analysis(
    base_estimator=LogisticRegression(),
    include_comparisons=True,
    include_tradeoffs=True
)

# Print summary
print(report.summary())

# Export to JSON
report.to_json('training_analysis.json')

# Render as SVG
report.to_svg('training_report.svg')

Key Methods

MethodDescription
evaluate_baseline(model)Evaluate baseline model without fairness constraints
compare_methods(base_estimator)Compare different training methods side-by-side
analyze_tradeoffs(base_estimator)Analyze accuracy-fairness trade-off curve
generate_recommendation()Generate recommended approach with rationale
full_analysis()Complete analysis with report generation

Loss Functions (PyTorch)

Group Fairness Losses (PyTorch)

PyTorch loss functions that incorporate fairness penalties directly into the training objective.

How Fairness Loss Functions Work

In standard deep learning, a model minimizes a single loss function (e.g., cross-entropy) that measures prediction error. Fairness loss functions extend this by adding a second term that penalizes unfair behavior:

Ltotal = Ltask + λ × Lfairness

The parameter λ (lambda) controls the trade-off: a small λ prioritizes accuracy, a large λ prioritizes fairness. The key challenge is that standard fairness metrics (like TPR, FPR) involve hard thresholding, which is not differentiable. These loss functions solve this by using soft, differentiable approximations — replacing hard 0/1 decisions with smooth probability outputs so that gradients can flow through and the model can learn to be fair.

The warmup_epochs parameter lets the model first learn the task (good predictions) before gradually introducing the fairness penalty, which leads to more stable training.

flowchart LR subgraph Loop["PyTorch Training Loop"] direction TB X["Batch
(X, y, groups)"] --> FWD["model(X)"] FWD --> TASK["Task Loss
L_task = BCE(ŷ, y)"] FWD --> FAIR["Fairness Loss
L_fair = penalty(ŷ, groups)"] TASK --> COMBINE["L_total = L_task + λ × L_fair"] FAIR --> COMBINE COMBINE --> BWD["loss.backward()"] BWD --> OPT["optimizer.step()"] end subgraph Choices["Fairness Loss Options"] DP["DemographicParityLoss
equal positive rates"] EO["EqualizedOddsLoss
equal TPR + FPR"] EOP["EqualOpportunityLoss
equal TPR"] BGL["BoundedGroupLoss
minimax fairness"] end Choices -->|"choose one"| FAIR style Loop fill:#f0f9ff,stroke:#0aafe3,stroke-width:2px style Choices fill:#f0f9ff,stroke:#0aafe3 style COMBINE fill:#fef3c7,stroke:#f59e0b,color:#92400e style FAIR fill:#e0f2fe,stroke:#059669,color:#0c4a6e style TASK fill:#e0e7ff,stroke:#6366f1,color:#3730a3 style DP fill:#059669,color:#fff style EO fill:#059669,color:#fff style EOP fill:#059669,color:#fff style BGL fill:#059669,color:#fff
Click diagram to zoom & pan
Loss Function Formula

L_total = L_task + λ × L_fairness
Where λ controls the accuracy-fairness trade-off.

Available Loss Functions

DemographicParityLoss
Equal positive prediction rates across groups
Outcome Independence
EqualizedOddsLoss
Equal true positive and false positive rates
Equal Error Rates
EqualOpportunityLoss
Equal true positive rate only
Equal Chances
FalsePositiveRateParityLoss
Equal false positive rates
Equal False Accusation
BoundedGroupLoss
Bounded worst-group loss
Minimax Fairness
python
import torch
from vfairness.in_processing import DemographicParityLoss

# Create loss function
loss_fn = DemographicParityLoss(
    lambda_fairness=0.1,  # Trade-off parameter
    warmup_epochs=5       # Epochs before applying fairness penalty
)

# Training loop
model = YourModel()
optimizer = torch.optim.Adam(model.parameters())

for epoch in range(num_epochs):
    loss_fn.set_epoch(epoch)

    for x, y, sensitive_attr in dataloader:
        optimizer.zero_grad()
        y_pred = torch.sigmoid(model(x))
        loss = loss_fn(y_pred, y, sensitive_attr)
        loss.backward()
        optimizer.step()

    # End of epoch tracking
    metrics = loss_fn.end_epoch()
    print(f"Epoch {epoch}: Loss={metrics.avg_total_loss:.4f}")

Adversarial Losses

Adversarial training approaches for learning fair representations.

The Adversary Idea — A Fairness "Spy"

Adversarial debiasing works like a two-player game. The main model (the predictor) tries to make good predictions, while a second small network (the adversary) tries to guess the sensitive attribute (e.g., gender or race) from the predictor's outputs. If the adversary succeeds, it means the predictions still leak information about group membership.

Training pushes the predictor to be accurate at its task while simultaneously making it impossible for the adversary to detect which group a person belongs to. When the adversary can do no better than random guessing, the model's predictions are independent of the sensitive attribute — which is exactly what demographic parity requires.

The gradient reversal layer (use_gradient_reversal=True) makes this efficient: during backpropagation, the signal from the adversary is flipped, actively pushing the predictor away from encoding group information. This approach is inspired by domain adaptation research (Ganin et al., 2016) and was applied to fairness by Zhang et al. (2018).

Loss FunctionMechanismUse Case
AdversarialDebiasingLossAdversary predicts sensitive attrRemove information leakage
ProjectedAdversarialLossGradient projectionMore stable adversarial training
FairRepresentationLossFair representation learningLearning intermediate representations
python
from vfairness.in_processing import AdversarialDebiasingLoss

loss_fn = AdversarialDebiasingLoss(
    lambda_fairness=1.0,
    adversary_hidden_dims=[64, 32],
    n_groups=2,
    use_gradient_reversal=True
)

# Move to GPU if available
loss_fn = loss_fn.to(device)

# Training loop
for x, y, sensitive_attr in dataloader:
    y_pred = torch.sigmoid(model(x))
    loss = loss_fn(y_pred, y, sensitive_attr)
    loss.backward()
    optimizer.step()

    # Monitor adversary accuracy (lower is more fair)
    adv_acc = loss_fn.get_adversary_accuracy(y_pred.detach(), sensitive_attr)

Counterfactual Losses

Losses based on counterfactual fairness and individual fairness criteria.

Causal & Counterfactual Fairness

Counterfactual losses, individual fairness and causal pathway blocking for fair in-processing

0:00 / 0:00
Counterfactual Fairness — "What If I Were in a Different Group?"

Counterfactual fairness (Kusner et al., 2017) asks a causal question: "Would this person have received the same prediction if they had belonged to a different demographic group, but were otherwise identical?" If the answer is yes for every individual, the model is counterfactually fair.

This is fundamentally different from group fairness metrics, which compare averages across groups. Counterfactual fairness operates at the individual level, making it a stronger and more intuitive definition — it captures what most people mean when they say "this decision was unfair to me."

In practice, the loss works by creating counterfactual copies of each data point (swapping the group attribute), feeding both the original and counterfactual through the model, and penalizing any difference in predictions. The group_swap strategy simply flips the group label, while group_mean replaces group-specific features with population averages to remove group influence.

CounterfactualFairnessLoss
Same prediction under causal intervention
Causal Fairness
IndividualFairnessLoss
Similar individuals get similar predictions
Individual-level
CausalFairnessLoss
Block unfair causal pathways in the model
Known Causal Structure

How Counterfactual Fairness Works

flowchart LR subgraph CAUSAL ["Causal Graph"] direction TB A["Protected
Attribute (A)"] X["Features (X)"] Y["Outcome (Ŷ)"] A -->|"direct"| Y A -->|"indirect"| X X --> Y end subgraph COUNTER ["Counterfactual Test"] direction TB A2["A → A'
intervene"] X2["X' = f(A', U)"] Y2["Ŷ' = model(X')"] A2 --> X2 X2 --> Y2 end subgraph FAIR ["Fairness Criterion"] direction TB CMP["Ŷ ≈ Ŷ' ?"] PASS["✅ Fair"] FAIL["❌ Unfair"] CMP -->|"yes"| PASS CMP -->|"no"| FAIL end CAUSAL --> COUNTER COUNTER --> FAIR style A fill:#ef4444,color:#fff style A2 fill:#f59e0b,color:#fff style CMP fill:#6366f1,color:#fff style PASS fill:#0aafe3,color:#fff style FAIL fill:#ef4444,color:#fff style CAUSAL fill:#f8fafc,stroke:#0aafe3,stroke-width:2px style COUNTER fill:#f8fafc,stroke:#f59e0b,stroke-width:2px style FAIR fill:#f8fafc,stroke:#6366f1,stroke-width:2px
Click diagram to zoom & pan
python
from vfairness.in_processing import CounterfactualFairnessLoss

loss_fn = CounterfactualFairnessLoss(
    lambda_fairness=0.1,
    counterfactual_strategy='group_swap',  # 'group_mean', 'group_swap', 'adversarial'
)

# Factory function for easy creation
from vfairness.in_processing import create_fairness_loss

loss_fn = create_fairness_loss(
    'demographic_parity',
    lambda_fairness=0.1,
    warmup_epochs=5
)

Constraint-Based Training Algorithms

Exponentiated Gradient, Grid Search, and Threshold Optimizer for guaranteed fairness constraint satisfaction

0:00 / 0:00

Constraint-Based Training

ExponentiatedGradient

The main reductions algorithm from Agarwal et al. (2018) that reduces fair classification to a sequence of cost-sensitive classification problems.

How It Works — Iterative Reweighting

Imagine you have a standard classifier that is unfair — it approves 80% of Group A but only 50% of Group B. The Exponentiated Gradient algorithm fixes this by repeatedly reweighting training samples: it increases the importance of Group B samples and decreases Group A samples, then retrains the classifier on these adjusted weights. After several rounds, the classifier naturally balances its predictions across groups.

The key insight from Agarwal et al. (2018) is that this process has theoretical convergence guarantees: it is mathematically proven to find a classifier that satisfies the fairness constraint (within tolerance ε) while being near-optimal in accuracy. The final output is a mixture (weighted combination) of the classifiers trained at each iteration.

The name "exponentiated gradient" comes from the update rule for the Lagrange multipliers: λ ← λ · exp(η · violation). Multipliers for violated constraints grow exponentially, forcing the next classifier to heavily prioritize those constraints. The parameter eta (η) controls how aggressive this correction is — it is the optimizer's learning rate, not the fairness weight itself.

Best for: when you want guaranteed fairness with any black-box classifier (Random Forest, XGBoost, SVM, etc.) and cannot modify the model's internal training loop.

flowchart TD START["Initialize
λ₀ = uniform weights"] --> ITER{"Iteration t
< max_iter?"} ITER -->|Yes| TRAIN["Train base estimator h_t
on cost-sensitive problem
with weights λ_t"] TRAIN --> EVAL["Evaluate fairness
constraint violation
on h_t predictions"] EVAL --> CHECK{"Constraint
satisfied?
violation < ε"} CHECK -->|Yes| CONV["✅ Converged
Return mixture
of classifiers"] CHECK -->|No| UPDATE["Update Lagrange
multipliers λ via
exponentiated gradient"] UPDATE --> MIX["Add h_t to
classifier mixture
Q = {h₁, ..., h_t}"] MIX --> ITER ITER -->|No| BEST["Return best
feasible mixture Q*"] style START fill:#0aafe3,color:#fff,stroke:#0c4a6e style TRAIN fill:#f0f9ff,stroke:#0aafe3,color:#0c4a6e style EVAL fill:#f0f9ff,stroke:#0aafe3,color:#0c4a6e style CHECK fill:#fef3c7,stroke:#f59e0b,color:#92400e style CONV fill:#0aafe3,color:#fff,stroke:#0c4a6e style UPDATE fill:#e0e7ff,stroke:#6366f1,color:#3730a3 style MIX fill:#f0f9ff,stroke:#0aafe3,color:#0c4a6e style BEST fill:#0aafe3,color:#fff,stroke:#0c4a6e
Click diagram to zoom & pan
python
from sklearn.linear_model import LogisticRegression
from vfairness.in_processing import (
    ExponentiatedGradient,
    DemographicParityConstraint,
)

# Create constraint
constraint = DemographicParityConstraint(tolerance=0.05)

# Create algorithm
eg = ExponentiatedGradient(
    base_estimator=LogisticRegression(),
    constraint=constraint,
    max_iterations=50,
    verbose=True
)

# Fit
result = eg.fit(X_train, y_train, sensitive_attr=gender)

# Predict
y_pred = eg.predict(X_test)

# Check results
print(f"Accuracy: {result.accuracy:.4f}")
print(f"Violation: {result.final_violation:.4f}")
print(f"Converged: {result.optimization_result.converged}")

Available Constraints

ConstraintMathematical Definition
DemographicParityConstraint|P(ŷ=1|G=a) - P(ŷ=1|G=b)| ≤ ε
EqualizedOddsConstraint|TPR_a - TPR_b| ≤ ε AND |FPR_a - FPR_b| ≤ ε
EqualOpportunityConstraint|TPR_a - TPR_b| ≤ ε
FalsePositiveRateParityConstraint|FPR_a - FPR_b| ≤ ε
BoundedGroupLossConstraintL_g ≤ (1+ε) × L_overall for all g

ThresholdOptimizer

Post-processing threshold optimization for applying fairness constraints to pre-trained models.

Fairness Without Retraining — Group-Specific Thresholds

Most classifiers produce probability scores (e.g., "72% likely to repay a loan") and then apply a fixed threshold (typically 0.5) to make a yes/no decision. ThresholdOptimizer makes a simple but powerful observation: different groups may need different thresholds to achieve fair outcomes.

For example, if Group A's scores tend to be systematically higher due to historical data biases, using the same threshold means Group A gets approved more often. By lowering Group B's threshold slightly (say to 0.42), you can equalize approval rates — achieving demographic parity without changing the model itself.

This is the least invasive fairness intervention: it works on any pre-trained model that outputs probabilities, requires no retraining, and is mathematically equivalent to the optimal post-processing solution from Hardt et al. (2016). Use it when you cannot retrain a model (e.g., a vendor-provided model) or as a quick baseline before trying more sophisticated approaches.

python
from sklearn.ensemble import RandomForestClassifier
from vfairness.in_processing import ThresholdOptimizer, EqualOpportunityConstraint

# First train a standard classifier
base_clf = RandomForestClassifier()
base_clf.fit(X_train, y_train)

# Get probabilities
y_prob_val = base_clf.predict_proba(X_val)[:, 1]

# Optimize thresholds for fairness
optimizer = ThresholdOptimizer(
    constraint=EqualOpportunityConstraint(tolerance=0.05),
    grid_size=100
)
optimizer.fit(y_prob_val, y_val, sensitive_attr=gender_val)

# Apply to test data
y_prob_test = base_clf.predict_proba(X_test)[:, 1]
y_pred_fair = optimizer.predict(y_prob_test, gender_test)

# Get the learned thresholds
print("Thresholds per group:", optimizer.get_thresholds())

Regularizers & Calibrators

Fairness Regularizers

Modular penalty terms that can be added to any differentiable loss function.

Regularization — A Familiar Concept Extended to Fairness

If you've used L1 or L2 regularization, you already understand the idea. In standard machine learning, regularization adds a penalty to prevent overfitting (e.g., penalizing large weights). Fairness regularizers work the same way, but instead of penalizing model complexity, they penalize unfair behavior.

Each regularizer computes a differentiable fairness penalty that measures how much the model's predictions depend on group membership. This penalty is added to whatever loss function you're already using, with a strength parameter controlling the trade-off. The model then naturally learns to reduce this penalty through standard gradient descent.

The advantage over loss functions: regularizers are modular building blocks — you can combine them with any existing task loss, stack multiple fairness penalties, or swap them out without changing your training loop. The HilbertSchmidtRegularizer (HSIC) is particularly powerful because it captures non-linear dependencies between predictions and group membership that simple correlation measures would miss.

Rule of thumb: start with strength=0.1 and increase gradually. If accuracy drops sharply, reduce it. If fairness metrics barely improve, increase it.

StatisticalParityRegularizer
Difference in mean predictions across groups
Demographic Parity
ConditionalIndependenceReg
Conditional dependence given outcome y
Equalized Odds
GroupFairnessRegularizer
Configurable group-level metrics
Flexible Criteria
HilbertSchmidtRegularizer
HSIC-based independence measure
Non-linear Dependence
CorrelationPenalty
Pearson correlation penalty
Linear Dependence
python
import torch.nn.functional as F
from vfairness.in_processing import StatisticalParityRegularizer

regularizer = StatisticalParityRegularizer(strength=0.1)

# In training loop
for x, y, sensitive_attr in dataloader:
    y_pred = torch.sigmoid(model(x))

    # Task loss
    task_loss = F.binary_cross_entropy(y_pred, y)

    # Fairness penalty
    fairness_penalty = regularizer(y_pred, sensitive_attr)

    # Combined loss
    total_loss = task_loss + fairness_penalty
    total_loss.backward()

Group-Specific Calibrators

Trainable calibration methods that learn separate parameters for each demographic group.

Why Group-Specific Calibration Matters

A model is calibrated if when it says "70% chance," the event actually happens 70% of the time. But even well-calibrated models can be miscalibrated for specific groups: a "70% risk" prediction might correspond to 75% actual risk for Group A but only 60% for Group B. This means the same score has a different meaning depending on which group you belong to — a serious fairness problem.

Group-specific calibrators fix this by learning separate calibration mappings for each demographic group. TemperatureScaling (simplest) divides logits by a group-specific temperature parameter. PlattScaling learns a linear transform. BetaCalibrator offers more flexibility. All ensure that a "70% score" truly means 70% probability regardless of group membership.

These calibrators are trainable PyTorch modules that can be integrated into end-to-end training (via CalibrationAwareTrainer) or applied as a post-processing step to an already-trained model. Group-fair calibration is especially important for applications like credit scoring, medical risk assessment, or insurance pricing where probability estimates directly drive decisions.

CalibratorParametersDescription
TemperatureScalingCalibratorT per groupDivides logits by temperature
PlattScalingCalibrator(a, b) per groupLinear transform in log-odds space
BetaCalibrator(c, d, e) per groupMore flexible beta calibration
FocalCalibratorγ per groupFocal-loss inspired calibration
TrainableGroupCalibratorConfigurableUnified interface for all methods
python
from vfairness.in_processing import (
    TrainableGroupCalibrator,
    CalibrationAwareTrainer,
)

# Create calibrator
calibrator = TrainableGroupCalibrator(
    n_groups=2,
    method='temperature',  # 'temperature', 'platt', 'beta', 'focal'
    learnable=True
)

# Integrate with model training
trainer = CalibrationAwareTrainer(model, calibrator)

# Training step
losses = trainer.train_step(
    x, y, group_ids,
    optimizer,
    task_loss_fn=F.cross_entropy,
    include_calibration=True
)

print(f"Task loss: {losses['task_loss']:.4f}")
print(f"Calibration loss: {losses['calibration_loss']:.4f}")

Training Visualization

Training Visualization (SVG Templates)

Lightweight, dependency-free SVG alternatives generated via Jinja2 templates — ideal for CI/CD artifacts, HTML reports, and headless environments. Each function accepts an optional save_path and returns the SVG string (3–8 KB).

FunctionDescriptionUse Case
training_report_to_svg()Full training analysis dashboardExecutive summary of fairness training
training_analysis_report_to_svg()Comprehensive full-page analysis reportDetailed dashboard: groups, methods, trade-offs, issues, actions
method_comparison_to_svg()Side-by-side method comparison chartCompare reductions vs Lagrangian methods
tradeoff_analysis_to_svg()Accuracy-fairness trade-off visualizationλ tuning and Pareto frontier analysis

Example Visualizations

Click any visualization to view in full size with details.

View all training templates in the SVG Gallery →

Usage Examples

python
from sklearn.linear_model import LogisticRegression
from vfairness.in_processing import FairnessTrainingAnalyzer
from vfairness.rendering import (
    training_report_to_svg,
    training_analysis_report_to_svg,
    method_comparison_to_svg,
    tradeoff_analysis_to_svg
)

# Create analyzer and run full analysis
analyzer = FairnessTrainingAnalyzer(
    X=X_train, y=y_train, sensitive_attr=gender,
    fairness_constraint='demographic_parity',
    tolerance=0.05
)
report = analyzer.full_analysis(base_estimator=LogisticRegression())

# ★ Generate the comprehensive full-page report SVG (new!)
svg = training_analysis_report_to_svg(report, save_path='analysis_report.svg')

# Generate compact dashboard SVG
svg = training_report_to_svg(report, save_path='training_report.svg')

# Or generate individual visualizations
svg = method_comparison_to_svg(
    report.method_comparisons,
    save_path='method_comparison.svg'
)

svg = tradeoff_analysis_to_svg(
    report.tradeoff_analysis,
    save_path='tradeoff.svg'
)

# Use directly from report object
svg = report.to_svg(save_path='full_report.svg')
Why SVG Templates?
  • No dependencies — Works without Matplotlib, Seaborn, or Plotly
  • CI/CD friendly — Generate reports in headless environments
  • Lightweight — 3-8 KB per visualization vs 100+ KB for raster images
  • Scalable — Vector graphics look crisp at any resolution
  • Embeddable — Inline directly in HTML reports or emails
Best Practice: Setting λ (Lambda)
  • λ = 0: Pure accuracy optimization (unfair)
  • λ = 0.01-0.1: Light fairness penalty
  • λ = 0.1-0.5: Moderate penalty
  • λ = 0.5-1.0: Strong fairness emphasis
  • λ > 1.0: Fairness dominates (may hurt accuracy)

Recommendation: Start with λ = 0.1 and use FairnessTrainingAnalyzer to explore the trade-off curve.

References

  • Hardt, M., Price, E., & Srebro, N. (2016). Equality of Opportunity in Supervised Learning. NeurIPS.
  • Agarwal, A., Beygelzimer, A., Dudík, M., Langford, J., & Wallach, H. (2018). A Reductions Approach to Fair Classification. ICML.
  • Zhang, B. H., Lemoine, B., & Mitchell, M. (2018). Mitigating Unwanted Biases with Adversarial Learning. AIES.
  • Kusner, M. J., Loftus, J., Russell, C., & Silva, R. (2017). Counterfactual Fairness. NeurIPS.
  • Zafar, M. B., Valera, I., Gomez Rodriguez, M., & Gummadi, K. P. (2017). Fairness Constraints: Mechanisms for Fair Classification. AISTATS.
  • Guo, C., Pleiss, G., Sun, Y., & Weinberger, K. Q. (2017). On Calibration of Modern Neural Networks. ICML.

3. Prediction-Time Interventions

Group-specific calibration and threshold adjustments applied at prediction time to improve fairness while maintaining accuracy.

Demo Notebook: vfairness_3_calibration_demo.ipynb — Group calibration, threshold optimization & Pareto analysis
python
from vfairness.post_processing import (
    # Calibration
    GroupCalibrator, CalibrationAnalyzer, expected_calibration_error,
    # Threshold Optimization
    GroupThresholdOptimizer, MultiObjectiveThresholdOptimizer, ThresholdAnalyzer,
    # Prediction Reweighting
    PredictionReweighter, RejectionOptionClassifier, CalibratedEqualizer,
    DistributionMatcher, ReweightingAnalyzer
)

What's Included

Group Calibration
Ensure probability predictions mean the same thing across demographic groups
Threshold Optimization
Group-specific decision thresholds for fairness without retraining
Prediction Reweighting
Adjust predicted probabilities to achieve fairness constraints
Trade-off Analysis
Pareto frontiers and fairness-accuracy trade-off analysis
flowchart LR subgraph PostP["Prediction-Time Interventions"] direction TB PRED[Model Predictions] --> CAL[Calibration] PRED --> THR[Threshold Opt] PRED --> REW[Reweighting] CAL --> ADJ[Adjusted Predictions] THR --> ADJ REW --> ADJ end subgraph Calibration["Calibration"] PLATT[Platt Scaling] ISO[Isotonic] BETA[Beta] TEMP[Temperature] end subgraph Threshold["Threshold Optimization"] GTO[GroupThresholdOptimizer] MTO[MultiObjectiveOptimizer] TA[ThresholdAnalyzer] end subgraph Reweight["Prediction Reweighting"] PR[PredictionReweighter] ROC[RejectionOptionClassifier] CE[CalibratedEqualizer] DM[DistributionMatcher] end CAL --> Calibration THR --> Threshold REW --> Reweight style PRED fill:#fee2e2,stroke:#ef4444 style ADJ fill:#d1fae5,stroke:#10b981 style PostP fill:#e0e7ff,stroke:#6366f1 style Calibration fill:#eef2ff,stroke:#6366f1 style Threshold fill:#eef2ff,stroke:#6366f1 style Reweight fill:#eef2ff,stroke:#6366f1
Click diagram to zoom & pan

Group-Specific Calibration

Calibration Module

Ensure probability predictions have consistent meaning across demographic groups. The calibration module addresses a distinct fairness dimension: interpretation consistency.

Calibration for Fairness

Understanding probability calibration across demographic groups and its impact on fair decision-making

0:00 / 0:00
Why Calibration Matters for Fairness

A 70% probability should mean the same risk whether the applicant is young or old, male or female. Yet many accurate models produce miscalibrated probabilities across groups, creating subtle algorithmic unfairness. This module provides tools to detect and correct such disparities.

How Calibration Works

Calibration measures the alignment between predicted probabilities and actual outcome rates. A well-calibrated model saying "70% chance" should have approximately 70% positive outcomes among all cases with that prediction.

flowchart TD INPUT["Model outputs
predicted probabilities"] --> BIN["Bin predictions into
equal-width intervals"] BIN --> COMPUTE["For each bin, compute:
• Mean predicted probability
• Actual positive rate"] COMPUTE --> ECE["ECE = Σ (bin_weight × |predicted − actual|)"] ECE --> GROUP["Compute ECE
per demographic group"] GROUP --> DISP{"ECE differs
across groups?"} DISP -->|"Small gap
(< 0.05)"| OK["✅ Well-calibrated
for all groups"] DISP -->|"Large gap
(≥ 0.05)"| FIX["⚠️ Apply group-specific
calibration"] FIX --> METHOD{"Choose
calibration method"} METHOD -->|"Parametric"| PLATT["Platt Scaling"] METHOD -->|"Non-parametric"| ISO["Isotonic Regression"] METHOD -->|"Neural nets"| TEMP["Temperature Scaling"] style INPUT fill:#2c5f7c,color:#fff style OK fill:#10b981,color:#fff style FIX fill:#f59e0b,color:#fff style PLATT fill:#6366f1,color:#fff style ISO fill:#6366f1,color:#fff style TEMP fill:#6366f1,color:#fff
Click diagram to zoom & pan

Quick Start

python
from vfairness.post_processing.calibration import (
    # Calibration metrics
    expected_calibration_error,
    calibration_disparity,
    # Group-specific calibration
    GroupCalibrator,
    # Trade-off analysis
    analyze_calibration_fairness_tradeoff,
    recommend_calibration_strategy,
)

# Evaluate calibration across groups
ece_result = expected_calibration_error(y_true, y_prob, gender)
print(f"Overall ECE: {ece_result.overall_value:.3f}")
print(f"ECE Disparity: {ece_result.max_group_disparity:.3f}")

# Apply group-specific calibration
calibrator = GroupCalibrator(method='isotonic')
calibrator.fit(y_true, y_prob, gender)
calibrated_probs = calibrator.transform(y_prob_test, gender_test)

CalibrationAnalyzer

Unified analyzer for comprehensive probability calibration assessment across demographic groups.

Background

A model that says "70% chance of default" should be right about 70% of the time — for every group. When this alignment breaks down differently across demographics, equal-sounding probabilities carry unequal meaning, creating a subtle but consequential form of unfairness. CalibrationAnalyzer measures this alignment using three complementary metrics:

MetricWhat It MeasuresIdeal Value
ECE (Expected Calibration Error)Weighted average gap between predicted probabilities and actual outcome rates0
MCE (Maximum Calibration Error)Worst-case gap across all probability bins — catches localised miscalibration0
Brier ScoreMean squared error of probabilities, decomposable into reliability, resolution, and uncertainty0

When the ECE disparity across groups exceeds 0.05, the analyzer recommends group-specific recalibration (Platt scaling, isotonic regression, or temperature scaling) and can fit and apply the calibrator in place.

Example Reliability Diagram

The reliability diagram below shows a well-calibrated group (blue, close to the diagonal) and a miscalibrated group (pink, systematically over-confident). The CalibrationAnalyzer produces this kind of per-group breakdown automatically.

class CalibrationAnalyzer Main Class

Constructor

python
CalibrationAnalyzer(
    y_true: ArrayLike,
    y_prob: ArrayLike,
    protected_attr: ArrayLike,
    *,
    attribute_name: Optional[str] = None,
    n_bins: int = 10,
    min_group_size: int = 30,
    config: Optional[Dict[str, Any]] = None,
)
Parameters
y_true ArrayLike Required
True binary labels (0 or 1).
y_prob ArrayLike Required
Predicted probabilities for the positive class (values between 0 and 1).
protected_attr ArrayLike Required
Protected attribute defining demographic groups (e.g., gender, race).
attribute_name Optional[str] Optional None
Descriptive name for the protected attribute (keyword-only). Used in reports.
n_bins int Optional 10
Number of bins for calibration error computation.
min_group_size int Optional 30
Minimum samples per group for group-level analysis.
config Optional[Dict[str, Any]] Optional None
Additional configuration options.

Analysis Workflow

flowchart TD INIT["CalibrationAnalyzer
y_true, y_prob, protected_attr"] --> EVAL["evaluate_calibration()"] EVAL --> ECE["ECE, MCE, Brier Score
per group"] INIT --> DISP["analyze_disparity()"] DISP --> DISPRES["Calibration disparity
Most/least miscalibrated groups"] INIT --> TRADE["analyze_tradeoffs()"] TRADE --> PARETO["Pareto frontier
Impossibility diagnosis"] INIT --> REC["get_recommendation()"] REC --> STRAT["Strategy recommendation
Priority & rationale"] INIT --> FULL["full_analysis()"] FULL --> REPORT["CalibrationReport
Comprehensive analysis"] REPORT --> FIT["fit_calibrator()"] FIT --> CAL["calibrate() / transform()"] CAL --> IMPROVED["Calibrated probabilities
Reduced disparity"] style INIT fill:#2c5f7c,color:#fff style FULL fill:#6366f1,color:#fff style REPORT fill:#6366f1,color:#fff style IMPROVED fill:#10b981,color:#fff
Click diagram to zoom & pan

Methods

Method Returns Description
full_analysis(**kwargs) CalibrationReport Run comprehensive calibration analysis (keyword-only flags: include_tradeoffs, include_recommendation, include_brier_decomposition, context)
evaluate_calibration() Dict[str, CalibrationMetricResult] Compute ECE, MCE, and Brier score with group breakdown
evaluate_ece() CalibrationMetricResult Compute Expected Calibration Error
evaluate_mce() CalibrationMetricResult Compute Maximum Calibration Error
evaluate_brier() CalibrationMetricResult Compute Brier Score
decompose_brier() BrierDecomposition Decompose Brier into reliability, resolution, uncertainty
analyze_disparity() CalibrationDisparityResult Analyze calibration disparities across groups
analyze_tradeoffs() TradeoffAnalysisResult Analyze calibration-fairness trade-offs and Pareto frontier
get_impossibility_diagnosis() Dict[str, Any] Diagnose Kleinberg impossibility theorem conditions
get_recommendation(context) CalibrationRecommendation Get context-aware calibration strategy recommendation
fit_calibrator(method) GroupCalibrator Fit a group-specific calibrator
calibrate(y_prob, protected_attr) np.ndarray Apply calibration to probabilities
transform(y_prob, protected_attr) np.ndarray Apply fitted calibrator to new data
evaluate_calibration_improvement() Dict[str, Any] Compare before/after calibration metrics

Properties

Property Type Description
n_samples int Total number of samples
n_groups int Number of demographic groups
groups List[str] List of group names
base_rate float Overall positive class rate
group_base_rates Dict[str, float] Base rates per demographic group

Example: Full Analysis

python
from vfairness.post_processing.calibration import CalibrationAnalyzer

# Create analyzer
analyzer = CalibrationAnalyzer(
    y_true=labels,
    y_prob=probabilities,
    protected_attr=gender,
    attribute_name='gender'
)

# Run full analysis
report = analyzer.full_analysis(context='lending')

# Print summary
print(report.summary())

# Check key findings
print(f"Well Calibrated: {report.is_well_calibrated}")
print(f"Significant Disparity: {report.has_significant_disparity}")
print(f"ECE: {report.overall_metrics['ece']:.4f}")

# Get recommendations
for issue in report.critical_issues:
    print(f"[{issue['type']}] {issue['description']}")

Example: Apply Calibration

python
# Fit calibrator and apply to new data
analyzer.fit_calibrator(method='isotonic')
calibrated_probs = analyzer.transform(y_prob_test, gender_test)

# Evaluate improvement
improvement = analyzer.evaluate_calibration_improvement()
print(f"ECE Before: {improvement['before']['overall_ece']:.4f}")
print(f"ECE After: {improvement['after']['overall_ece']:.4f}")
print(f"Improvement: {improvement['improvement']['overall_ece']:.4f}")

CalibrationReport

The CalibrationReport object returned by full_analysis() contains:

Attribute Type Description
overall_metrics Dict[str, float] ECE, MCE, Brier score
group_metrics Dict[str, Dict] Per-group calibration metrics
disparity_analysis CalibrationDisparityResult Disparity analysis results
tradeoff_analysis TradeoffAnalysisResult Trade-off analysis with Pareto frontier
recommendation CalibrationRecommendation Strategy recommendation
is_well_calibrated bool True if ECE < 0.05
has_significant_disparity bool True if ECE disparity > 0.05
critical_issues List[Dict] Critical issues identified
recommendations List[str] Prioritized action items
Context-Aware Recommendations

The context parameter in full_analysis() tailors recommendations to your domain:

  • risk_assessment: Prioritizes calibration for consistent risk interpretation
  • lending: Emphasizes regulatory compliance and group-specific calibration
  • healthcare: Focuses on individual risk accuracy for treatment decisions
  • hiring: Considers error rate parity alongside calibration
  • general: Balanced recommendations

Calibration Methods

Multiple calibration techniques for different use cases.

σ Platt Scaling
Parametric calibration via logistic regression — ideal for SVMs & linear models
Platt 1999
Isotonic Regression
Non-parametric, flexible monotonic mapping — best with large calibration sets
Zadrozny & Elkan 2002
β Beta Calibration
Three-parameter model for bounded probabilities — handles asymmetric distortions
Kull et al. 2017
Temperature Scaling
Single-parameter softmax rescaling — preserves ranking, designed for neural networks
Guo et al. 2017
Histogram Binning
Simple bin-based calibration — easy to interpret and debug
Zadrozny & Elkan 2001

Choosing a Calibration Method

flowchart TD START["Choose calibration method"] --> Q1{"Model type?"} Q1 -->|"SVM / linear"| PLATT["Platt Scaling
Parametric, fast"] Q1 -->|"Neural network"| Q_NN{"Large calibration
dataset?"} Q1 -->|"Ensemble / tree"| Q_DATA{"Sample size
per group?"} Q_NN -->|"Yes (≥ 1000)"| TEMP["Temperature Scaling
Single parameter, preserves ranking"] Q_NN -->|"No"| PLATT Q_DATA -->|"Large (≥ 500)"| ISO["Isotonic Regression
Non-parametric, flexible"] Q_DATA -->|"Medium (100–500)"| BETA["Beta Calibration
3-param, bounded"] Q_DATA -->|"Small (< 100)"| HIST["Histogram Binning
Simple, interpretable"] style START fill:#2c5f7c,color:#fff style PLATT fill:#6366f1,color:#fff style TEMP fill:#6366f1,color:#fff style ISO fill:#10b981,color:#fff style BETA fill:#10b981,color:#fff style HIST fill:#f59e0b,color:#fff
Click diagram to zoom & pan

PlattScaling

python
from vfairness.post_processing.calibration import PlattScaling

# Platt scaling fits logistic regression: P(y=1|f) = 1/(1 + exp(A*f + B))
calibrator = PlattScaling(regularization=0.0)
calibrator.fit(y_true, y_prob)
calibrated = calibrator.transform(y_prob_test)

# Access fitted parameters
print(f"A: {calibrator.a_:.4f}, B: {calibrator.b_:.4f}")

IsotonicCalibrator

python
from vfairness.post_processing.calibration import IsotonicCalibrator

# Isotonic regression: non-parametric, preserves rank ordering
calibrator = IsotonicCalibrator(out_of_bounds='clip')
calibrator.fit(y_true, y_prob)
calibrated = calibrator.transform(y_prob_test)

# Access calibration thresholds
print(f"Number of isotonic segments: {len(calibrator.x_thresholds_)}")

Factory Function

python
from vfairness.post_processing.calibration import create_calibrator

# Create calibrator by name
calibrator = create_calibrator('isotonic')
calibrator = create_calibrator('platt', regularization=0.1)
calibrator = create_calibrator('beta', parameters='abm')
calibrator = create_calibrator('temperature')
calibrator = create_calibrator('histogram', n_bins=15)

Calibration Metrics

Quantify calibration quality with specialized metrics.

See Also: calibration_difference()

The calibration_difference() function in the Classification Metrics section computes the maximum ECE gap across groups as a single fairness metric. Use it for quick fairness audits; use the functions below for deeper calibration analysis and remediation.

E ECE
Weighted average calibration error across bins
expected_calibration_error()
Ideal: 0 (< 0.05 good)
M MCE
Worst-case bin calibration error
maximum_calibration_error()
Ideal: 0 (< 0.1 good)
B Brier Score
Mean squared probability error — measures both calibration and sharpness
brier_score()
Ideal: 0 (< 0.25 ok)
Calibration Curve
Reliability diagram data — points should lie on the diagonal
calibration_curve()
Ideal: on diagonal

Expected Calibration Error

python
from vfairness.post_processing.calibration import expected_calibration_error

# Compute ECE with group breakdown
result = expected_calibration_error(
    y_true, y_prob,
    protected_attr=gender,  # Optional: group analysis
    n_bins=10,
    strategy='uniform'  # or 'quantile'
)

print(f"Overall ECE: {result.overall_value:.4f}")
print(f"Is well-calibrated: {result.is_well_calibrated}")

# Per-group ECE
for group, ece in result.group_values.items():
    print(f"  {group}: {ece:.4f}")

# ECE disparity between groups
print(f"Max group disparity: {result.max_group_disparity:.4f}")

Interpretation Thresholds

Calibration Difference Assessment Recommended Action
≤ 0.03 Excellent No action needed
0.03 – 0.05 Acceptable Monitor; document in fairness report
0.05 – 0.08 Concerning Investigate root cause; consider group-specific calibration
0.08 – 0.10 Poor Apply remediation; review training data balance
≥ 0.10 Critical Mandatory remediation before deployment

Brier Score Decomposition

python
from vfairness.post_processing.calibration import brier_score_decomposition

# Decompose Brier = Reliability - Resolution + Uncertainty
decomp = brier_score_decomposition(y_true, y_prob, n_bins=10)

print(f"Brier Score: {decomp.brier_score:.4f}")
print(f"  Reliability: {decomp.reliability:.4f}")  # Lower is better
print(f"  Resolution: {decomp.resolution:.4f}")   # Higher is better
print(f"  Uncertainty: {decomp.uncertainty:.4f}") # Fixed for dataset
print(f"Skill Score: {decomp.skill_score:.4f}")   # Improvement over climatology

Calibration Disparity Analysis

python
from vfairness.post_processing.calibration import calibration_disparity

# Comprehensive disparity analysis
result = calibration_disparity(y_true, y_prob, race)

print(f"ECE Disparity: {result.ece_disparity:.4f}")
print(f"MCE Disparity: {result.mce_disparity:.4f}")
print(f"Most miscalibrated: {result.most_miscalibrated_group}")
print(f"Significant disparity: {result.has_significant_disparity}")

# Recommendations
for rec in result.recommendations:
    print(f"  - {rec}")

Group-Specific Calibration

Apply calibration separately for each demographic group to ensure consistent probability interpretation.

GroupCalibrator

python
from vfairness.post_processing.calibration import GroupCalibrator

# Fit group-specific calibrators
calibrator = GroupCalibrator(
    method='isotonic',          # Calibration method
    min_group_size=50,          # Min samples for group-specific
    fallback_strategy='global'  # For small groups: 'global', 'borrow', 'none'
)

calibrator.fit(y_true_train, y_prob_train, protected_attr_train)
calibrated_probs = calibrator.transform(y_prob_test, protected_attr_test)

# Get calibration improvement report
result = calibrator.get_calibration_result()
print(f"Overall ECE improvement: {result.overall_improvement:.4f}")

for group, improvement in result.improvement.items():
    pre_ece = result.pre_calibration_ece[group]
    post_ece = result.post_calibration_ece[group]
    print(f"  {group}: {pre_ece:.3f} → {post_ece:.3f} (Δ={improvement:.3f})")

IntersectionalCalibrator

python
from vfairness.post_processing.calibration import IntersectionalCalibrator
import pandas as pd

# Create intersectional groups (e.g., gender × race)
intersectional_attrs = pd.DataFrame({
    'gender': gender,
    'race': race
})

# Calibrate with hierarchical borrowing for small groups
calibrator = IntersectionalCalibrator(
    method='isotonic',
    min_group_size=30,
    borrowing_strategy='hierarchical'  # Borrow from parent groups
)

calibrator.fit(y_true, y_prob, intersectional_attrs)
calibrated = calibrator.transform(y_prob_test, intersectional_attrs_test)

Calibration-Fairness Trade-offs

Analyze the fundamental tension between calibration and other fairness criteria.

Impossibility Theorem

Kleinberg et al. (2016) proved that calibration, balance for the positive class, and balance for the negative class cannot be simultaneously satisfied when base rates differ between groups. This module helps navigate these unavoidable trade-offs.

The Impossibility Triangle

flowchart TD Q{"Do base rates
differ between groups?"} Q -->|"No — equal
base rates"| ALL["✅ All three criteria
can be satisfied"] Q -->|"Yes — unequal
base rates"| PICK["⚠️ Must choose at most TWO"] PICK --> C1["Calibration
+
Balance for Positives"] PICK --> C2["Calibration
+
Balance for Negatives"] PICK --> C3["Balance Positives
+
Balance Negatives"] C1 --> LOSE1["❌ Sacrifices:
Balance for Negatives"] C2 --> LOSE2["❌ Sacrifices:
Balance for Positives"] C3 --> LOSE3["❌ Sacrifices:
Calibration"] style Q fill:#2c5f7c,color:#fff style ALL fill:#10b981,color:#fff style PICK fill:#ef4444,color:#fff style C1 fill:#6366f1,color:#fff style C2 fill:#6366f1,color:#fff style C3 fill:#6366f1,color:#fff style LOSE1 fill:#f59e0b,color:#fff style LOSE2 fill:#f59e0b,color:#fff style LOSE3 fill:#f59e0b,color:#fff
Click diagram to zoom & pan

Trade-off Analysis

python
from vfairness.post_processing.calibration import analyze_calibration_fairness_tradeoff

# Analyze trade-off at different thresholds
result = analyze_calibration_fairness_tradeoff(
    y_true, y_prob, gender,
    fairness_metric='fpr_parity',  # or 'fnr_parity', 'demographic_parity'
    thresholds=[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
)

print(f"Trade-off severity: {result.tradeoff_severity}")
print(f"Base rate disparity: {result.base_rate_disparity:.3f}")

# Current model position
print(f"Current ECE: {result.current_point.calibration_error:.3f}")
print(f"Current fairness violation: {result.current_point.fairness_violation:.3f}")

# Recommendations
for rec in result.recommendations:
    print(f"  - {rec}")

Understanding the Pareto Frontier

The Pareto frontier (or Pareto front) is the set of model configurations where no objective can be improved without worsening another. In the calibration-fairness context, each point on the frontier represents a threshold or calibration strategy where you cannot reduce calibration error without increasing fairness violation, or vice versa.

flowchart TD subgraph AXES ["Pareto Frontier: Calibration vs. Fairness"] direction LR A["Point A
Low ECE (0.02)
High fairness violation (0.15)
Well-calibrated but unfair"] B["Point B
Balanced
ECE 0.06 · Violation 0.06
Compromise"] C["Point C
Low violation (0.02)
High ECE (0.12)
Fair but miscalibrated"] end A -->|"Trade more
calibration"| B B -->|"Trade more
calibration"| C D["Point D (dominated)
ECE 0.10 · Violation 0.12
Strictly worse than B"] -.->|"Move to
frontier"| B style A fill:#3b82f6,color:#fff style B fill:#10b981,color:#fff style C fill:#8b5cf6,color:#fff style D fill:#ef4444,color:#fff
Click diagram to zoom & pan

Typical Pareto Frontier Visualisation

The chart below shows what a calibration–fairness Pareto frontier looks like in practice. Points on the blue curve are Pareto-optimal; points in the shaded region are dominated (strictly improvable).

0.00 0.05 0.10 0.15 0.20 0.00 0.05 0.10 0.15 0.20 Calibration Error (ECE) → Fairness Violation → Dominated region A · Low ECE B · Balanced C · Low violation Your model D E Pareto frontier Dominated (improvable) Current model position Dominated region
Key Concepts
  • On the frontier (blue curve) — the configuration is Pareto-optimal: any improvement on one axis requires a sacrifice on the other. Points A, B, and C above are all valid choices depending on your priorities.
  • Below the frontier / dominated (red dots, shaded area) — the configuration is sub-optimal: another configuration exists that is better on both axes. Points D and E are dominated and should be discarded.
  • Current model (orange ring) — where your model currently sits. If it falls inside the dominated region, you can improve it for free by moving to the nearest frontier point (dashed arrow).
  • Choosing a point — is a values decision, not a technical one. The frontier tells you what is achievable; your organization's risk tolerance, regulatory requirements, and ethical priorities determine where on the frontier to operate.

Practical Workflow

  1. Map the frontier — use analyze_calibration_fairness_tradeoff() to sweep thresholds and calibration strategies, plotting ECE vs. fairness violation.
  2. Identify dominated points — any current model position below the frontier can be improved for free by switching to a frontier configuration.
  3. Select your operating point — regulatory contexts (e.g., EU AI Act) may mandate maximum fairness violation, effectively choosing the point for you. In other contexts, a cost-benefit analysis guides the decision.
  4. Visualise with plot_tradeoff_curve() — generates the frontier plot with your model's current position annotated, making it easy to communicate trade-offs to stakeholders.
python
from vfairness.post_processing.calibration import (
    analyze_calibration_fairness_tradeoff,
    plot_tradeoff_curve
)
import matplotlib.pyplot as plt

# 1. Compute the frontier
result = analyze_calibration_fairness_tradeoff(
    y_true, y_prob, gender,
    fairness_metric='fpr_parity',
    thresholds=[t / 20 for t in range(1, 20)]  # 0.05 to 0.95
)

# 2. Visualise: frontier + current model position
ax = plot_tradeoff_curve(
    result,
    highlight_current=True,      # Mark current operating point
    show_dominated=True,         # Show dominated region
    annotate_extremes=True       # Label the frontier endpoints
)
plt.title("Calibration–Fairness Pareto Frontier")
plt.xlabel("Calibration Error (ECE)")
plt.ylabel("Fairness Violation (FPR disparity)")
plt.savefig("pareto_frontier.png", dpi=300)

# 3. Find the closest frontier point to a target
target = result.nearest_pareto_point(
    max_calibration_error=0.05,
    max_fairness_violation=0.08
)
print(f"Recommended threshold: {target.threshold:.2f}")
print(f"ECE: {target.calibration_error:.3f}")
print(f"Fairness violation: {target.fairness_violation:.3f}")

Strategy Recommendation

python
from vfairness.post_processing.calibration import recommend_calibration_strategy

# Get context-aware recommendation
rec = recommend_calibration_strategy(
    y_true, y_prob, gender,
    context='lending'  # or 'risk_assessment', 'healthcare', 'hiring', 'general'
)

print(f"Strategy: {rec.strategy}")
print(f"Priority: {rec.priority}")
print(f"Rationale: {rec.rationale}")
if rec.expected_improvement:
    print(f"Expected ECE improvement: {rec.expected_improvement:.3f}")
if rec.tradeoff_warning:
    print(f"Warning: {rec.tradeoff_warning}")

Impossibility Diagnostics

python
from vfairness.post_processing.calibration import impossibility_diagnostics

# Diagnose impossibility theorem conditions
diag = impossibility_diagnostics(y_true, y_prob, race)

print(f"Base rates differ: {diag['base_rates_differ']}")
print(f"Impossibility theorem applies: {diag['impossibility_applies']}")
print(f"Base rate disparity: {diag['base_rate_disparity']:.3f}")
print(f"\nExplanation: {diag['explanation']}")

Calibration Visualization

Visual tools for calibration analysis and communication.

Function Description
plot_reliability_diagram() Standard calibration curve with histogram
plot_group_calibration() Calibration curves per demographic group
plot_calibration_comparison() Compare multiple models/methods
plot_calibration_disparity() Bar charts of ECE/MCE/Brier by group
plot_tradeoff_curve() Pareto frontier with current position, dominated region, and annotated extremes (see Trade-off Analysis)
create_calibration_dashboard() Comprehensive multi-panel dashboard
python
from vfairness import (
    plot_reliability_diagram,
    plot_group_calibration,
    create_calibration_dashboard
)
import matplotlib.pyplot as plt

# Basic reliability diagram
ax = plot_reliability_diagram(y_true, y_prob, n_bins=10, show_histogram=True)
plt.title("Model Calibration")
plt.savefig("reliability_diagram.png", dpi=300)

# Group-specific calibration
ax = plot_group_calibration(y_true, y_prob, gender, n_bins=10)
plt.title("Calibration by Gender")
plt.savefig("group_calibration.png", dpi=300)

# Comprehensive dashboard (before and after calibration)
fig = create_calibration_dashboard(
    y_true, y_prob, gender,
    calibrated_probs=calibrated_probs  # Optional: show improvement
)
plt.savefig("calibration_dashboard.png", dpi=300)

SVG Templates vfairness.rendering

Dependency-free SVG alternatives for calibration visualisations. No plotting library required.

View all calibration templates in the SVG Gallery →

python
from vfairness.rendering import reliability_diagram_to_svg, pareto_frontier_to_svg

svg = reliability_diagram_to_svg(y_true, y_prob, n_bins=10, save_path="reliability.svg")

svg = pareto_frontier_to_svg(
    calibration_errors=[0.05, 0.08, 0.12, 0.03],
    fairness_violations=[0.10, 0.06, 0.03, 0.12],
    labels=["Platt", "Isotonic", "Beta", "Temperature"],
    save_path="pareto.svg"
)

Threshold Optimization

Threshold Optimization Module

Achieve fairness without retraining by optimizing group-specific decision thresholds. The most non-invasive post-processing intervention for any classifier that outputs probabilities.

When to Use Threshold Optimization

Threshold optimization is ideal when you cannot retrain the model (vendor-provided, regulatory constraints, or frozen pipeline) but need to achieve fairness constraints. It works by finding optimal per-group thresholds that satisfy fairness criteria while minimizing accuracy loss.

Key Classes

GroupThresholdOptimizer
Find optimal per-group thresholds satisfying a single fairness constraint (demographic parity, equalized odds, etc.)
MultiObjectiveThresholdOptimizer
Pareto optimization across multiple fairness constraints simultaneously with accuracy trade-offs
ThresholdAnalyzer
Comprehensive analysis of threshold impacts on fairness and accuracy across all groups
python
from vfairness.post_processing import (
    GroupThresholdOptimizer,
    MultiObjectiveThresholdOptimizer,
    ThresholdAnalyzer,
    FairnessConstraintType
)

# Single-constraint optimization
optimizer = GroupThresholdOptimizer(
    constraint=FairnessConstraintType.DEMOGRAPHIC_PARITY,
    tolerance=0.05  # Allow 5% disparity
)
optimizer.fit(y_true, y_prob, sensitive_attr)
result = optimizer.get_optimal_thresholds()
# {'male': 0.52, 'female': 0.47}

# Multi-objective optimization (Pareto frontier)
multi_opt = MultiObjectiveThresholdOptimizer(
    constraints=[
        FairnessConstraintType.DEMOGRAPHIC_PARITY,
        FairnessConstraintType.EQUALIZED_ODDS
    ],
    n_thresholds=50  # Resolution of search
)
pareto_solutions = multi_opt.fit(y_true, y_prob, sensitive_attr)

# Comprehensive analysis
analyzer = ThresholdAnalyzer(y_true, y_prob, sensitive_attr)
report = analyzer.full_analysis()
print(report.summary())

GroupThresholdOptimizer

Optimizes decision thresholds per demographic group to satisfy a single fairness constraint.

class GroupThresholdOptimizer Main Class

Constructor

python
GroupThresholdOptimizer(
    constraint: FairnessConstraintType = FairnessConstraintType.DEMOGRAPHIC_PARITY,
    tolerance: float = 0.05,
    min_threshold: float = 0.01,
    max_threshold: float = 0.99,
    n_thresholds: int = 100,
    objective: str = 'accuracy'
)
Parameters
constraint FairnessConstraintType DEMOGRAPHIC_PARITY
Fairness constraint to satisfy. Options: DEMOGRAPHIC_PARITY, EQUALIZED_ODDS, EQUAL_OPPORTUNITY, PREDICTIVE_PARITY.
tolerance float 0.05
Maximum allowed disparity between groups (e.g., 0.05 = 5% difference allowed).
objective str 'accuracy'
What to optimize subject to fairness constraint. Options: 'accuracy', 'balanced_accuracy', 'f1', 'custom'.

Methods

fit(y_true, y_prob, sensitive_attr) — Find optimal thresholds satisfying the constraint
predict(y_prob, sensitive_attr) — Apply optimized thresholds to make predictions
get_optimal_thresholds() — Return dict mapping group names to optimal thresholds

MultiObjectiveThresholdOptimizer

Finds Pareto-optimal threshold configurations that trade off multiple fairness constraints against accuracy.

Impossibility Theorems

Multiple fairness constraints often cannot be satisfied simultaneously (Chouldechova 2017, Kleinberg et al. 2016). This optimizer finds the Pareto frontier — the set of solutions where no constraint can be improved without worsening another.

python
from vfairness.post_processing import MultiObjectiveThresholdOptimizer

optimizer = MultiObjectiveThresholdOptimizer(
    constraints=[
        FairnessConstraintType.DEMOGRAPHIC_PARITY,
        FairnessConstraintType.EQUAL_OPPORTUNITY
    ],
    n_thresholds=100
)

# Get Pareto-optimal solutions
pareto_solutions = optimizer.fit(y_true, y_prob, sensitive_attr)

# Each solution includes:
# - thresholds: Dict[str, float] per-group thresholds
# - accuracy: float overall accuracy
# - fairness_metrics: Dict[str, float] per-constraint violation
# - is_pareto_optimal: bool

ThresholdAnalyzer

Comprehensive analysis tool for understanding threshold selection trade-offs.

python
from vfairness.post_processing import ThresholdAnalyzer

analyzer = ThresholdAnalyzer(y_true, y_prob, sensitive_attr)

# Full analysis across all threshold configurations
report = analyzer.full_analysis()
print(report.summary())

# Compare specific threshold configurations
comparison = analyzer.compare_thresholds([
    {'male': 0.5, 'female': 0.5},   # Equal threshold
    {'male': 0.52, 'female': 0.47}  # Optimized for DP
])

# Sensitivity analysis
sensitivity = analyzer.threshold_sensitivity(
    base_thresholds={'male': 0.5, 'female': 0.5},
    perturbation_range=0.1
)

Prediction Reweighting

Prediction Reweighting Module

Adjust predicted probabilities post-hoc to achieve fairness constraints. Unlike threshold optimization which only changes the decision boundary, reweighting modifies the probability estimates themselves.

When to Use Reweighting vs Threshold Optimization

Use Threshold Optimization when you only need fair binary decisions and can live with unchanged probability estimates.
Use Reweighting when the probability estimates themselves must be fair (e.g., for ranking, risk scoring, or downstream probability-based decisions).

Available Methods

Method Approach Best For
PredictionReweighter Multiplicative or additive adjustment per group Simple, interpretable adjustments
RejectionOptionClassifier Only modify predictions near decision boundary Minimal changes, high-confidence predictions unchanged
CalibratedEqualizer Equalize calibrated probability distributions When calibration and fairness both matter
DistributionMatcher Match probability distributions across groups Strongest fairness guarantee
python
from vfairness.post_processing import (
    PredictionReweighter,
    RejectionOptionClassifier,
    CalibratedEqualizer,
    DistributionMatcher,
    ReweightingAnalyzer
)

# Simple multiplicative reweighting
reweighter = PredictionReweighter(
    method='multiplicative',
    constraint='demographic_parity'
)
reweighter.fit(y_true, y_prob, sensitive_attr)
y_prob_fair = reweighter.transform(y_prob, sensitive_attr)

# Rejection Option Classification (ROC)
roc = RejectionOptionClassifier(
    theta=0.1,  # Only modify predictions within ±0.1 of threshold
    constraint='demographic_parity'
)
roc.fit(y_true, y_prob, sensitive_attr)
y_pred_fair = roc.predict(y_prob, sensitive_attr)

PredictionReweighter

Adjusts predicted probabilities using simple multiplicative or additive transformations per group.

class PredictionReweighter Main Class

Constructor

python
PredictionReweighter(
    method: str = 'multiplicative',  # 'multiplicative' or 'additive'
    constraint: str = 'demographic_parity',
    target_rate: Optional[float] = None  # Target positive rate (None = overall rate)
)

How It Works

Multiplicative: p_new = p_old × scale_factor where scale factor brings group rates to target.

Additive: p_new = p_old + offset where offset shifts group rates to target.

RejectionOptionClassifier

Implements Rejection Option Based Classification (ROC) from Kamiran et al. (2012). Only modifies predictions near the decision boundary, leaving high-confidence predictions unchanged.

Minimal Intervention Philosophy

ROC embodies the principle of minimal intervention: only change predictions where the model is uncertain (near 0.5). High-confidence predictions (near 0 or 1) are assumed to be reliable and left untouched. This preserves model accuracy while improving fairness.

python
from vfairness.post_processing import RejectionOptionClassifier

roc = RejectionOptionClassifier(
    theta=0.1,  # Critical region: [0.5 - θ, 0.5 + θ]
    constraint='demographic_parity'
)
roc.fit(y_true, y_prob, sensitive_attr)

# Returns binary predictions (not probabilities)
y_pred_fair = roc.predict(y_prob, sensitive_attr)

Reference

Kamiran, F., Karim, A., & Zhang, X. (2012). Decision Theory for Discrimination-Aware Classification. ICDM 2012.

CalibratedEqualizer

Combines calibration with fairness by equalizing calibrated probability distributions across groups.

python
from vfairness.post_processing import CalibratedEqualizer

equalizer = CalibratedEqualizer(
    calibration_method='isotonic',  # 'platt', 'isotonic', 'beta'
    fairness_constraint='equalized_odds'
)
equalizer.fit(y_true, y_prob, sensitive_attr)
y_prob_fair = equalizer.transform(y_prob, sensitive_attr)

Reference

Pleiss, G., et al. (2017). On Fairness and Calibration. NeurIPS 2017.

ReweightingAnalyzer

Comprehensive analysis tool for comparing reweighting methods and their trade-offs.

python
from vfairness.post_processing import ReweightingAnalyzer

analyzer = ReweightingAnalyzer(y_true, y_prob, sensitive_attr)

# Compare all methods
report = analyzer.full_analysis()
print(report.summary())
print(f"Recommended method: {report.best_method}")

# Output:
# Reweighting Analysis Report
# ==================================================
# Analyzed 5 reweighting methods
# Recommended method: rejection_option
#
# Recommendations:
#   • For maximum fairness improvement, use 'distribution_matching'
#   • 'rejection_option' preserves accuracy best (no accuracy loss)
#   • Overall recommendation: 'rejection_option' provides the best trade-off

4. Evaluation & Measurement

Fairness metrics & analysis, statistical tools, and visualization to quantify fairness through validated metrics and interpretable explanations.

python
from vfairness.evaluation import (
    FairnessAnalyzer,
    demographic_parity_difference,
    classification_fairness_report,
    print_report
)

What's Included

Fairness Metrics
Classification, regression, and ranking fairness measures
FairExplAIner
Human-readable explanations for every metric result
Statistical Tools
Confidence intervals, hypothesis testing, robustness analysis
Visualization
Charts and dashboards for fairness reporting
flowchart LR subgraph EVAL["Evaluation & Measurement"] direction TB FA[FairnessAnalyzer] --> FM[Fairness Metrics] FA --> STAT[Statistical Tools] FM --> VIZ[Visualization] FA --> EXP[FairExplAIner] end subgraph Metrics["Fairness Metrics"] CL[Classification] RG[Regression] RK[Ranking] end subgraph Stats["Statistical Tools"] CI[Confidence Intervals] HT[Hypothesis Testing] ROB[Robustness] end FM --> Metrics STAT --> Stats style FA fill:#2c5f7c,stroke:#333,stroke-width:2px,color:#fff style EVAL fill:#e0f2fe,stroke:#2c5f7c style Metrics fill:#f0f9ff,stroke:#2c5f7c style Stats fill:#f0f9ff,stroke:#2c5f7c
Click diagram to zoom & pan

Fairness Metrics & Analysis

Fairness Metrics Module

The Fairness Metrics module provides comprehensive tools for measuring and analyzing model fairness across classification, regression, and ranking tasks.

Demo Notebook: vfairness_4_metrics_demo.ipynb — Classification, regression & ranking metrics with confidence intervals
Click diagram to zoom & pan
flowchart TB
    subgraph FM["Fairness Metrics Module"]
        FA["FairnessAnalyzer
(Unified Interface)"] subgraph Metrics["Metric Types"] CL["Classification
DP, EO, EOD, PP"] RG["Regression
MAE, RMSE parity"] RK["Ranking
Exposure parity"] end subgraph Features["Key Features"] CI["Confidence Intervals
Bootstrap & Bayesian"] EX["FairExplAIner
Human-readable explanations"] IS["Intersectional
Multi-attribute analysis"] end subgraph Output["Reporting"] RP["Fairness Reports"] VZ["Visualizations"] ML["MLflow Integration"] end end FA --> Metrics FA --> Features FA --> Output style FA fill:#4A90D9,stroke:#333,stroke-width:2px,color:#fff style Metrics fill:#e0e7ff,stroke:#6366f1 style Features fill:#e0f2fe,stroke:#0aafe3 style Output fill:#f0fdf4,stroke:#22c55e

Metric Categories

Classification
Demographic Parity · Equal Opportunity · Equalized Odds · Predictive Parity
approve/deny, hire/reject
Regression
MAE Parity · RMSE Parity · Mean Prediction Difference
salary, risk scores
Ranking
Exposure Parity · Attention-Weighted Rank Fairness
search, recommendations, candidate ranking

FairnessAnalyzer

The unified entry point for all fairness metric computations.

Background

Once a model has been trained, FairnessAnalyzer measures how fairly its predictions distribute across demographic groups. It supports three task types — classification, regression, and ranking — and auto-detects the appropriate metric family from the data.

Four core classification fairness metrics correspond to different legal and ethical frameworks:

MetricQuestion It AnswersLegal Analogy
Demographic Parity DifferenceAre groups approved at the same rate?Disparate impact (80% rule)
Equal Opportunity DifferenceAmong qualified people, are groups accepted equally?Equal access for equally qualified
Equalized Odds DifferenceAre error rates (FPR and TPR) equal across groups?Equal error burden
Predictive Parity DifferenceDoes a positive prediction mean the same thing for each group?Equal precision

All metrics support bootstrap confidence intervals so you can assess statistical significance. Optionally enable FairExplAIner to get plain-language summaries suitable for non-technical stakeholders.

Example Dashboard

The visualisation below shows a typical metric dashboard produced by FairnessAnalyzer, including per-group prediction rates, gap annotations, and a FairExplAIner summary card.

class FairnessAnalyzer Main Class

A unified wrapper class providing a single interface for all fairness computations on classification, regression, and ranking tasks.

Constructor

python
FairnessAnalyzer(
    y_true: ArrayLike,
    y_pred: ArrayLike,
    sensitive_attr: ArrayLike,
    y_prob: Optional[ArrayLike] = None,
    task_type: Optional[str] = None,
    min_group_size: int = 30,
    missing_strategy: str = 'exclude',
    backend: str = 'auto',
    fair_explainer: bool = False
)
Parameters
y_true ArrayLike Required
True labels/outcomes. For classification: binary (0/1). For regression: continuous values.
y_pred ArrayLike Required
Model predictions. Must match y_true in length and type.
sensitive_attr ArrayLike Required
Protected attribute(s) defining groups. Can be array, Series, or DataFrame (for intersectional analysis).
y_prob Optional[ArrayLike] Optional None
Probability predictions for calibration metrics.
task_type Optional[str] Optional auto-detected
One of 'classification', 'regression', 'ranking'. Auto-detected if None.
min_group_size int Optional 30
Minimum samples per group. Groups below this threshold are excluded from analysis.
missing_strategy str Optional 'exclude'
How to handle missing values: 'exclude', 'as_group', or 'error'.
fair_explainer bool Optional False
Enable FairExplAIner for human-readable explanations of metric results.

Methods

Method Returns Description
demographic_parity_difference(include_ci=False) float | MetricResult Gap in positive prediction rates
demographic_parity_ratio(include_ci=False) float | MetricResult Ratio of positive rates (80% rule)
equal_opportunity_difference(include_ci=False) float | MetricResult Gap in true positive rates
equalized_odds_difference(include_ci=False) float | MetricResult Max of TPR and FPR differences
compute_all_metrics(include_ci=False) Dict All applicable metrics at once
get_report(include_ci=False, ...) Dict Comprehensive fairness report
enable_fair_explainer() None Enable FairExplAIner mode
explain_metric(metric_name) MetricExplanation Get explanation for specific metric
compare_with_fairlearn() Dict Compare results with Fairlearn library

Basic Example

python
from vfairness import FairnessAnalyzer
import numpy as np

# Sample data
y_true = np.array([1, 0, 1, 0, 1, 0, 1, 0] * 50)
y_pred = np.array([1, 0, 1, 1, 1, 0, 0, 0] * 50)
gender = np.array(['M', 'M', 'M', 'M', 'F', 'F', 'F', 'F'] * 50)

# Create analyzer
analyzer = FairnessAnalyzer(
    y_true=y_true,
    y_pred=y_pred,
    sensitive_attr=gender,
    fair_explainer=True
)

# Compute single metric
dp = analyzer.demographic_parity_difference()
print(f"Demographic Parity Difference: {dp:.3f}")

# Compute with confidence interval
result = analyzer.demographic_parity_difference(
    include_ci=True,
    n_bootstrap=5000,
    confidence_level=0.95
)
print(f"Value: {result.value:.3f}")
print(f"95% CI: [{result.ci_lower:.3f}, {result.ci_upper:.3f}]")

# Generate full report
report = analyzer.get_report(include_ci=True)
print(f"Fairness Score: {report['assessment']['fairness_score']:.1%}")
Fairness Metrics Are Mathematically Incompatible Limitation

Demographic parity, equalized odds, and calibration cannot all be satisfied simultaneously (Choquet et al., 2016; Kleinberg et al., 2016) except in trivial cases.

Gap: The library computes all metrics in parallel but does not warn when they conflict. Users may chase simultaneously improving all metrics — which is mathematically impossible.
Mitigation: Choose the fairness definition that matches your use case before running analysis. Use the Impossibility Theorem guide in the Concepts section to decide which metric to prioritise. The FairExplAIner provides context-specific guidance.
Binary Classification Only Limitation

All classification fairness metrics require binary (0/1) labels. Multi-class classification fairness is not supported.

Gap: Multi-class tasks (credit risk grades A/B/C/D, multi-disease diagnosis) cannot be analysed directly. Labels must be manually binarised, losing nuance.
Mitigation: Binarise labels using one-vs-rest: run separate analyses for each class (e.g., Grade A vs. not-A, Grade D vs. not-D). Focus on the most consequential class boundary.

FairExplAIner

Human-readable fairness explanations integrated into the FairnessAnalyzer. FairExplAIner translates metric values into plain-language summaries suitable for non-technical stakeholders.

Background

Fairness metrics like "Demographic Parity Difference = 0.15" are opaque to business owners, compliance officers, and end users. FairExplAIner bridges this gap by converting every metric result into three components:

  • Summary — a one-sentence plain-language description of what the metric value means in context
  • Interpretation — a contextual explanation of whether the result is concerning and why
  • Recommendation — concrete next steps to investigate or mitigate the finding

FairExplAIner is not a separate class — it is a capability layer inside FairnessAnalyzer. Enable it via the constructor flag or the enable_fair_explainer() method. Once enabled, every metric computation attaches an explanation to its result object.

Usage

python
# Enable FairExplAIner via constructor
analyzer = FairnessAnalyzer(
    y_true, y_pred, sensitive_attr,
    fair_explainer=True
)

# Or enable after creation
analyzer.enable_fair_explainer()

# Get explanation for a specific metric
explanation = analyzer.explain_metric('demographic_parity_difference')
print(explanation.summary)        # Plain-language summary
print(explanation.recommendation)  # Actionable next steps

Key Methods

Method Returns Description
enable_fair_explainer() None Activate explanation mode on the analyzer
explain_metric(metric_name) MetricExplanation Get a human-readable explanation for a computed metric
Note

FairExplAIner is part of FairnessAnalyzer — it is not a standalone class. Enable it via the fair_explainer=True constructor parameter or the enable_fair_explainer() method.

FairnessExplainer — Unified Explanation Facade

While FairExplAIner explains evaluation metrics inside FairnessAnalyzer, the FairnessExplainer facade generates educational, context-aware explanations for any result object produced across the entire library — from bias audit reports and calibration analyses to drift detection results and CI/CD gate decisions.

What FairnessExplainer Gives You
  • One unified API to explain any result from any module
  • Severity grading (info → critical) for every finding
  • Plain-language summaries suitable for non-technical stakeholders
  • Actionable recommendations distilled from each analysis
  • Extensible — register custom handlers for your own result types

How It Works

The FairnessExplainer uses a registry pattern: each result type maps to a domain-specific handler function. When you call explain(), it looks up the handler by class name and returns a structured ExplanationReport.

Click diagram to zoom & pan
flowchart LR
    subgraph MODULES["Library Modules"]
        direction TB
        BD["BiasDetector
.full_audit()"] FE["FeatureEngineeringAnalyzer
.full_analysis()"] CA["CalibrationAnalyzer
.full_analysis()"] TA["ThresholdAnalyzer
.full_analysis()"] RA["ReweightingAnalyzer
.full_analysis()"] FT["FairnessTrainingAnalyzer
.full_analysis()"] DD["FairnessDriftDetector
.check_drift()"] FM["FairnessMonitor
.update_and_check()"] DV["DataBiasValidator
.validate()"] MG["ModelFairnessGate
.evaluate()"] end subgraph FACADE["FairnessExplainer"] direction TB REG["Registry
type_name -> handler"] EXP["explain(result)"] REG --> EXP end subgraph OUTPUT["ExplanationReport"] direction TB T["title + summary"] S["severity"] M["MetricExplanation[]"] R["recommendations[]"] end BD --> FACADE FE --> FACADE CA --> FACADE TA --> FACADE RA --> FACADE FT --> FACADE DD --> FACADE FM --> FACADE DV --> FACADE MG --> FACADE FACADE --> OUTPUT style MODULES fill:#f8fcfe,stroke:#0891b2,stroke-width:2px style FACADE fill:#f0fdf4,stroke:#10b981,stroke-width:2px style OUTPUT fill:#eff6ff,stroke:#3b82f6,stroke-width:2px

Two Ways to Get Explanations

Every class provides a convenience get_explanation() method that delegates to the facade — but you can also call the facade directly.

A Convenience Method
python
# Call get_explanation() on any class
detector = BiasDetector(protected_features=['gender'])
report = detector.full_audit(df)

explanation = detector.get_explanation(report)
print(explanation)

Quickest path — the class already knows which handler to use.

B Direct Facade Call
python
from vfairness.explainer import FairnessExplainer

# Pass any result object directly
report = detector.full_audit(df)

explanation = FairnessExplainer.explain(report)
print(explanation)

Useful when you receive results from different sources and want one entry point.

Severity Levels

Every explanation is graded on a five-level severity scale. The ExplanationReport.severity field reflects the worst severity across all individual metrics in the report.

info
No concerns
score < 0.2
low
Minor flags
0.2 – 0.4
medium
Review needed
0.4 – 0.6
high
Action required
0.6 – 0.8
critical
Do not proceed
score ≥ 0.8

Supported Result Types

The facade handles 10 result types across every module in the library.

Result Type Produced By Module
BiasAuditReport BiasDetector.full_audit() Preprocessing
FeatureAnalysisReport FeatureEngineeringAnalyzer.full_analysis() Preprocessing
CalibrationReport CalibrationAnalyzer.full_analysis() Post-Processing
ThresholdAnalysisReport ThresholdAnalyzer.full_analysis() Post-Processing
ReweightingAnalysisReport ReweightingAnalyzer.full_analysis() Post-Processing
FairnessTrainingReport FairnessTrainingAnalyzer.full_analysis() In-Processing
MultiscaleDriftResult FairnessDriftDetector.check_drift() Operations
WindowMetrics FairnessMonitor.update_and_check() Operations
DataValidationResult DataBiasValidator.validate() Operations
GateDecision ModelFairnessGate.evaluate() Operations

Complete Example

A full workflow showing bias detection, explanation, and inspection of the returned object.

python
import pandas as pd
from vfairness import BiasDetector
from vfairness.explainer import FairnessExplainer

# 1. Run an analysis
df = pd.read_csv("loan_applications.csv")
detector = BiasDetector(protected_features=["gender", "ethnicity"])
report = detector.full_audit(df)

# 2. Get the explanation  (Option A — convenience method)
explanation = detector.get_explanation(report)

# 3. Inspect the ExplanationReport
print(explanation.title)          # "Bias Audit Explanation"
print(explanation.severity)       # "medium"
print(explanation.summary)        # one-paragraph narrative
print(explanation.recommendations)# ["Review representation ...", ...]

# 4. Iterate individual metric explanations
for metric in explanation.explanations:
    print(f"{metric.metric_name}: {metric.severity}")
    print(f"  → {metric.recommendation}")

# 5. Serialise to dict (for dashboards / APIs)
data = explanation.to_dict()

What the Output Looks Like

When you print() an ExplanationReport, it renders as a structured text report:

========================================================================
  Bias Audit Explanation  [MEDIUM]
========================================================================

The overall risk score of 0.45 represents significant bias risk.
Mitigation is recommended before model training.

ACTION ITEMS:
  1. Review representation balance across protected groups
  2. Investigate proxy variables flagged in the analysis
  3. Consider reweighting or augmentation before training

------------------------------------------------------------------------
Overall Bias Risk Score  [MEDIUM]

  Definition:   Aggregate measure of dataset bias risk (0 = no risk,
                1 = critical). Combines historical patterns, balance,
                statistical disparities, and proxy exposure.

  Value:        0.45
  Evaluation:   Significant bias risk detected.
  Benchmark:    0.0-0.2 low | 0.2-0.4 moderate | 0.4-0.6 significant
  Recommendation: Apply mitigation before model training.
------------------------------------------------------------------------
Representation Balance  [LOW]
  ...
------------------------------------------------------------------------

Data Flow

Every get_explanation() call follows the same three-step path:

Click diagram to zoom & pan
sequenceDiagram
    participant User
    participant Class as BiasDetector / CalibrationAnalyzer / ...
    participant FE as FairnessExplainer
    participant Handler as Domain Handler
    participant ER as ExplanationReport

    User->>Class: get_explanation(report)
    Class->>FE: explain(report)
    FE->>FE: lookup handler by type(report).__name__
    FE->>Handler: _explain_bias_audit(report)
    Handler->>Handler: Extract metrics, compute severities
    Handler->>ER: Build ExplanationReport
    ER-->>User: title, summary, severity, recommendations, explanations[]
                        

API Reference

FairnessExplainer

Unified facade for generating explanations. All methods are class-level (no instantiation needed).

Method Returns Description
explain(obj, **kwargs) ExplanationReport Generate an explanation for any supported result object. Raises TypeError if unsupported.
can_explain(obj) bool Check if a result type has a registered handler.
register(type_name, handler) None Register a custom handler function for a new result type name.
registered_types() list[str] List all currently registered result type names.
ExplanationReport

Container dataclass returned by FairnessExplainer.explain().

Attribute Type Description
titlestrShort human-readable title (e.g. "Bias Audit Explanation")
summarystrOne-paragraph narrative summarising the findings
explanationslist[MetricExplanation]Individual per-metric explanations with definitions, values, and recommendations
severitystrAggregate severity: info | low | medium | high | critical
recommendationslist[str]Top-level action items distilled from the analysis

Methods

Method Returns Description
to_dict()dictSerialise the report to a plain dictionary (JSON-ready)
__str__()strPretty-printed text report (shown above)

Convenience Methods per Class

Each major class exposes get_explanation() that delegates to FairnessExplainer.explain(). Classes that produce results internally (like analyzers) accept an optional report parameter — if omitted, they re-run the analysis. Classes that require external input (like drift detectors) require the result to be passed in.

Class Signature Behaviour
BiasDetectorget_explanation(report=None)Runs full_audit() if report is omitted
FeatureEngineeringAnalyzerget_explanation(report=None)Runs full_analysis() if report is omitted
CalibrationAnalyzerget_explanation(report=None)Runs full_analysis() if report is omitted
ThresholdAnalyzerget_explanation(report=None)Runs full_analysis() if report is omitted
ReweightingAnalyzerget_explanation(report=None)Runs full_analysis() if report is omitted
FairnessTrainingAnalyzerget_explanation(report=None)Runs full_analysis() if report is omitted
FairnessDriftDetectorget_explanation(result)Requires a MultiscaleDriftResult
FairnessMonitorget_explanation()Builds report from current window metrics inline
TemporalFairnessAnalyzerget_explanation()Builds report from trend & degradation analysis inline
DataBiasValidatorget_explanation(result)Requires a DataValidationResult
ModelFairnessGateget_explanation(result)Requires a GateDecision

Extending with Custom Handlers

Register your own handler function for a custom result type:

python
from vfairness.explainer import FairnessExplainer, ExplanationReport
from vfairness.evaluation.vfairness_metrics.explainer import MetricExplanation

def _explain_my_result(result) -> ExplanationReport:
    return ExplanationReport(
        title="My Custom Analysis",
        summary=f"Custom analysis found score={result.score:.2f}.",
        explanations=[
            MetricExplanation(
                metric_name="Custom Score",
                definition="My custom fairness metric.",
                interpretation_guide="Lower is better.",
                value=result.score,
                evaluation="Within acceptable range.",
                recommendation="No action needed.",
                severity="info",
            )
        ],
        severity="info",
        recommendations=["Continue monitoring."],
    )

# Register it
FairnessExplainer.register("MyCustomResult", _explain_my_result)

# Now it works with your type
explanation = FairnessExplainer.explain(my_result)
FairExplAIner vs FairnessExplainer

FairExplAIner is part of FairnessAnalyzer — it explains individual evaluation metrics (demographic parity, equalized odds, etc.) in depth.
FairnessExplainer is a standalone facade that explains any result object from any module across the entire library. They are complementary: use FairExplAIner for deep metric dives, use FairnessExplainer for end-to-end pipeline explanations.

Classification Metrics

Fairness metrics for binary classification tasks.

demographic_parity_difference

Compute the gap in positive prediction rates between groups.

python
demographic_parity_difference(
    y_true: ArrayLike,
    y_pred: ArrayLike,
    sensitive_attr: ArrayLike,
    min_group_size: int = 30,
    missing_strategy: str = 'exclude'
) -> float
Parameters
y_true ArrayLike Required
True binary labels (0/1).
y_pred ArrayLike Required
Model predictions (0/1).
sensitive_attr ArrayLike Required
Protected attribute defining groups.
min_group_size int Optional 30
Minimum samples per group. Smaller groups are excluded.
missing_strategy str Optional 'exclude'
How to handle missing values: 'exclude', 'as_group', or 'error'.
Returns

float - The maximum difference in positive prediction rates between any two groups. Range: [0, 1]. Ideal: 0.

demographic_parity_ratio

Compute the ratio of positive prediction rates (80% rule / disparate impact ratio).

python
demographic_parity_ratio(
    y_true: ArrayLike,
    y_pred: ArrayLike,
    sensitive_attr: ArrayLike,
    min_group_size: int = 30,
    missing_strategy: str = 'exclude'
) -> float
Parameters
y_true ArrayLike Required
True binary labels (0/1).
y_pred ArrayLike Required
Model predictions (0/1).
sensitive_attr ArrayLike Required
Protected attribute defining groups.
min_group_size int Optional 30
Minimum samples per group. Smaller groups are excluded.
missing_strategy str Optional 'exclude'
How to handle missing values: 'exclude', 'as_group', or 'error'.
Returns

float - Ratio of minimum to maximum positive rate. Range: [0, 1]. Ideal: 1.0. Values < 0.8 may indicate disparate impact.

equal_opportunity_difference

Compute the gap in True Positive Rates (TPR) between groups.

python
equal_opportunity_difference(
    y_true: ArrayLike,
    y_pred: ArrayLike,
    sensitive_attr: ArrayLike,
    min_group_size: int = 30,
    missing_strategy: str = 'exclude'
) -> float
Parameters
y_true ArrayLike Required
True binary labels (0/1).
y_pred ArrayLike Required
Model predictions (0/1).
sensitive_attr ArrayLike Required
Protected attribute defining groups.
min_group_size int Optional 30
Minimum samples per group. Smaller groups are excluded.
missing_strategy str Optional 'exclude'
How to handle missing values: 'exclude', 'as_group', or 'error'.
Returns

float - Maximum difference in TPR between groups. Range: [0, 1]. Ideal: 0.

equalized_odds_difference

Compute the maximum of TPR difference and FPR difference.

python
equalized_odds_difference(
    y_true: ArrayLike,
    y_pred: ArrayLike,
    sensitive_attr: ArrayLike,
    min_group_size: int = 30,
    missing_strategy: str = 'exclude'
) -> float
Parameters
y_true ArrayLike Required
True binary labels (0/1).
y_pred ArrayLike Required
Model predictions (0/1).
sensitive_attr ArrayLike Required
Protected attribute defining groups.
min_group_size int Optional 30
Minimum samples per group. Smaller groups are excluded.
missing_strategy str Optional 'exclude'
How to handle missing values: 'exclude', 'as_group', or 'error'.
Returns

float - max(|TPR_A - TPR_B|, |FPR_A - FPR_B|). Range: [0, 1]. Ideal: 0.

predictive_parity_difference

Compute the gap in precision (positive predictive value) between groups.

python
predictive_parity_difference(
    y_true: ArrayLike,
    y_pred: ArrayLike,
    sensitive_attr: ArrayLike,
    min_group_size: int = 30,
    missing_strategy: str = 'exclude'
) -> float
Parameters
y_true ArrayLike Required
True binary labels (0/1).
y_pred ArrayLike Required
Model predictions (0/1).
sensitive_attr ArrayLike Required
Protected attribute defining groups.
min_group_size int Optional 30
Minimum samples per group. Smaller groups are excluded.
missing_strategy str Optional 'exclude'
How to handle missing values: 'exclude', 'as_group', or 'error'.
Returns

float - Maximum difference in precision between groups. Range: [0, 1]. Ideal: 0.

calibration_difference

Compute the maximum difference in Expected Calibration Error (ECE) across groups. Measures whether the model's predicted probabilities are equally reliable for all groups.

python
calibration_difference(
    y_true: ArrayLike,
    y_pred: ArrayLike,
    sensitive_attr: ArrayLike,
    y_prob: ArrayLike,
    *,
    n_bins: int = 10,
    min_group_size: int = 30,
    missing_strategy: str = 'exclude'
) -> float
Parameters
y_true ArrayLike Required
True binary labels (0/1).
y_pred ArrayLike Required
Predicted binary labels (0/1).
sensitive_attr ArrayLike Required
Protected attribute defining groups.
y_prob ArrayLike Required
Predicted probabilities (continuous values between 0 and 1). Required for ECE computation.
n_bins int Optional 10
Number of calibration bins for ECE computation. More bins give finer granularity but require more data.
min_group_size int Optional 30
Minimum samples per group. Smaller groups are excluded.
missing_strategy str Optional 'exclude'
How to handle missing values: 'exclude', 'as_group', or 'error'.
Returns

float — Maximum difference in ECE between any two groups. Range: [0, 1]. Ideal: 0 (< 0.05 acceptable).

How It Works

For each group, the function computes the Expected Calibration Error (ECE): probabilities are binned into n_bins equal-width intervals, and for each bin the weighted gap between mean predicted probability and actual positive rate is summed. The returned value is the maximum ECE difference between any pair of groups.

Library Comparisons

AIF360: No direct equivalent  |  Fairlearn: No direct equivalent (use calibration_curve separately)  |  What-If Tool: Calibration visualization available but no group-differential metric.

Related: Calibration Module

For deeper calibration analysis — including calibration methods (Platt, Isotonic, Temperature Scaling), group-specific calibration, and trade-off analysis — see the Calibration Module documentation.

Metrics with Confidence Intervals

All classification metrics have *_with_ci variants that return statistical results:

python
from vfairness import demographic_parity_difference_with_ci

result = demographic_parity_difference_with_ci(
    y_true, y_pred, sensitive_attr,
    min_group_size=30,
    n_bootstrap=5000,
    confidence_level=0.95,
    method='auto',  # 'bootstrap', 'bayesian', or 'auto'
    random_state=42
)

# Returns StatisticalResult object
print(f"Point Estimate: {result.point_estimate:.3f}")
print(f"95% CI: [{result.lower_bound:.3f}, {result.upper_bound:.3f}]")
print(f"Standard Error: {result.standard_error:.4f}")
print(f"Method: {result.method_used}")
print(f"Interval Type: {result.interval_type}")  # 'confidence' or 'credible'

Regression Metrics

Fairness metrics for continuous outcome predictions.

mae_parity_difference

Compute the gap in Mean Absolute Error between groups.

python
mae_parity_difference(
    y_true: ArrayLike,
    y_pred: ArrayLike,
    sensitive_attr: ArrayLike,
    min_group_size: int = 30
) -> float
Parameters
y_true ArrayLike Required
True continuous outcome values.
y_pred ArrayLike Required
Model predictions (continuous).
sensitive_attr ArrayLike Required
Protected attribute defining groups.
min_group_size int Optional 30
Minimum samples per group. Smaller groups are excluded.
Returns

float — Maximum difference in MAE between any two groups.

rmse_parity_difference

Compute the gap in Root Mean Square Error between groups.

python
rmse_parity_difference(
    y_true: ArrayLike,
    y_pred: ArrayLike,
    sensitive_attr: ArrayLike,
    min_group_size: int = 30
) -> float
Parameters
y_true ArrayLike Required
True continuous outcome values.
y_pred ArrayLike Required
Model predictions (continuous).
sensitive_attr ArrayLike Required
Protected attribute defining groups.
min_group_size int Optional 30
Minimum samples per group. Smaller groups are excluded.
Returns

float — Maximum difference in RMSE between any two groups.

mean_prediction_difference

Compute the gap in average predictions between groups.

python
mean_prediction_difference(
    y_true: ArrayLike,
    y_pred: ArrayLike,
    sensitive_attr: ArrayLike,
    min_group_size: int = 30
) -> float
Parameters
y_true ArrayLike Required
True continuous outcome values.
y_pred ArrayLike Required
Model predictions (continuous).
sensitive_attr ArrayLike Required
Protected attribute defining groups.
min_group_size int Optional 30
Minimum samples per group. Smaller groups are excluded.
Returns

float — Maximum difference in mean predictions between any two groups.

Ranking Metrics

Fairness metrics for ranked lists (search results, recommendations).

exposure_parity_difference

Compute the gap in exposure/visibility between groups in a ranking.

python
exposure_parity_difference(
    rankings: ArrayLike,
    groups: ArrayLike,
    min_group_size: int = 2,
    exposure_type: str = 'log'  # 'log', 'linear', 'geometric'
) -> float
Parameters
rankings ArrayLike Required
Position in ranking (0 = first position, highest exposure).
groups ArrayLike Required
Group membership for each item in the ranking.
min_group_size int Optional 2
Minimum number of items per group required for inclusion.
exposure_type str Optional 'log'
Exposure decay model:
  • 'log' — 1/log₂(rank+1), realistic user attention
  • 'linear' — 1/rank, simple position-based
  • 'geometric' — 0.9^rank, exponential decay
Returns

float — Maximum difference in group exposure. 0.0 = perfect exposure parity.

attention_weighted_rank_fairness

Fairness metric accounting for realistic user attention patterns.

python
attention_weighted_rank_fairness(
    rankings: ArrayLike,
    groups: ArrayLike,
    min_group_size: int = 2,
    attention_model: str = 'position'  # 'position' or 'cascade'
) -> RankingFairnessResult
Parameters
rankings ArrayLike Required
Position in ranking (0 = first position).
groups ArrayLike Required
Group membership for each ranked item.
min_group_size int Optional 2
Minimum items per group required for inclusion.
attention_model str Optional 'position'
Attention model: 'position' (position-based bias) or 'cascade' (cascade click model).
Returns

RankingFairnessResult with attributes:

  • value: Fairness metric value
  • group_exposures: Dict of exposure per group
  • group_attentions: Dict of attention per group
  • threshold: Fairness threshold used
  • is_fair: Boolean indicating if threshold is met
No Relevance-Aware Ranking Fairness Limitation

Exposure and attention parity are group-level visibility metrics that do not account for item relevance or quality.

Gap: True ranking fairness (Singh & Joachims, 2018) requires balancing fairness with relevance. A ranking that is perfectly "fair" in exposure but ignores relevance is not useful. No FA*IR algorithm is implemented.
Mitigation: Combine vfairness ranking metrics with standard IR metrics (NDCG, MAP). For relevance-aware fair ranking, consider the FA*IR algorithm or LinkedIn's equity-of-attention framework.

Fairness Reports

classification_fairness_report

Generate a comprehensive fairness report for classification tasks.

python
classification_fairness_report(
    y_true: ArrayLike,
    y_pred: ArrayLike,
    sensitive_attr: ArrayLike,
    y_prob: Optional[ArrayLike] = None,
    min_group_size: int = 30,
    include_ci: bool = False,
    n_bootstrap: int = 5000,
    confidence_level: float = 0.95,
    ci_method: str = 'auto',
    multiple_testing_correction: str = 'none',
    include_group_analysis: bool = False,
    thresholds: Optional[Dict] = None
) -> Dict
Parameters
y_true ArrayLike Required
True binary labels (0/1).
y_pred ArrayLike Required
Model predictions (0/1).
sensitive_attr ArrayLike Required
Protected attribute defining groups.
y_prob Optional[ArrayLike] Optional None
Probability predictions for calibration metrics.
min_group_size int Optional 30
Minimum samples per group.
include_ci bool Optional False
Include confidence intervals for all metrics.
n_bootstrap int Optional 5000
Number of bootstrap samples for CI estimation.
confidence_level float Optional 0.95
Confidence level for interval estimation.
ci_method str Optional 'auto'
CI method: 'bootstrap', 'bayesian', or 'auto' (selects based on sample size).
multiple_testing_correction str Optional 'none'
Correction method for multiple comparisons (e.g., 'bonferroni', 'fdr_bh').
include_group_analysis bool Optional False
Include per-group performance breakdown.
thresholds Optional[Dict] Optional built-in defaults
Custom pass/fail thresholds per metric. Uses standard defaults if omitted.
Returns

Dict[str, Any] — Comprehensive report dictionary containing: metrics (all fairness metric values), assessment (pass/fail summary with fairness score), group_stats (per-group performance breakdown), data_info (sample sizes and group details). Optionally includes confidence_intervals, effect_sizes, and explanations when enabled.

Return Structure

python
{
    'task_type': 'classification',
    'metrics': {
        'demographic_parity_difference': 0.05,
        'demographic_parity_ratio': 0.92,
        'equalized_odds_difference': 0.08,
        'equal_opportunity_difference': 0.06,
        'predictive_parity_difference': 0.04,
    },
    'group_stats': {
        'Male': {
            'size': 500,
            'positive_rate': 0.45,
            'tpr': 0.80,
            'fpr': 0.15,
            'precision': 0.75
        },
        'Female': {...}
    },
    'assessment': {
        'fairness_score': 0.80,  # 4/5 metrics passed
        'passed_metrics': ['demographic_parity_difference', ...],
        'failed_metrics': ['equalized_odds_difference'],
        'summary': '4/5 metrics within thresholds'
    },
    'data_info': {
        'original_size': 1000,
        'final_size': 1000,
        'n_groups': 2,
        'groups': ['Male', 'Female'],
        'excluded_groups': []
    },
    # If include_ci=True:
    'confidence_intervals': {...},
    'effect_sizes': {...},
    # If fair_explainer enabled:
    'explanations': {...}
}
print_report

Print a formatted fairness report to console.

python
print_report(
    report: Dict,
    include_explanations: bool = False
) -> None
Parameters
report Dict Required
Report dictionary returned by classification_fairness_report() or compute_all_metrics().
include_explanations bool Optional False
Include FairExplAIner human-readable explanations in the output.
Returns

None — Prints the formatted report directly to stdout. Output includes metric values, pass/fail status, and optionally human-readable explanations.

regression_fairness_report

Generate a comprehensive fairness report for regression tasks. The regression counterpart of classification_fairness_report().

python
regression_fairness_report(
    y_true: ArrayLike,
    y_pred: ArrayLike,
    sensitive_attr: ArrayLike,
    *,
    min_group_size: int = 30,
    missing_strategy: str = 'exclude',
    thresholds: Optional[Dict[str, float]] = None,
    include_ci: bool = False,
    n_bootstrap: int = 5000,
    confidence_level: float = 0.95,
    ci_method: str = 'auto',
    multiple_testing_correction: str = 'none',
    random_state: Optional[int] = None
) -> Dict[str, Any]
Parameters
y_true ArrayLike Required
True continuous target values.
y_pred ArrayLike Required
Model predictions (continuous).
sensitive_attr ArrayLike Required
Protected attribute defining groups.
min_group_size int Optional 30
Minimum samples per group. Smaller groups are excluded.
missing_strategy str Optional 'exclude'
How to handle missing values: 'exclude' or 'impute'.
thresholds Optional[Dict[str, float]] Optional None
Custom pass/fail thresholds per metric. Uses library defaults if omitted.
include_ci bool Optional False
Whether to compute bootstrap confidence intervals for each metric.
n_bootstrap int Optional 5000
Number of bootstrap resamples (only used when include_ci=True).
confidence_level float Optional 0.95
Confidence level for intervals.
ci_method str Optional 'auto'
CI method: 'auto', 'bootstrap', or 'bayesian'.
multiple_testing_correction str Optional 'none'
Correction method: 'bonferroni', 'fdr', or 'none'.
random_state Optional[int] Optional None
Random seed for reproducibility of bootstrap sampling.
Returns

Dict[str, Any] — Report dictionary containing metrics (mae_parity_difference, rmse_parity_difference, mean_prediction_difference, r2_parity_difference), group_stats, assessment, and data_info. Same structure as classification report.

Statistical Tools

Confidence Intervals

bootstrap_ci

Compute bootstrap confidence interval for any statistic function.

python
bootstrap_ci(
    data: np.ndarray,
    statistic: Callable[[np.ndarray], float],
    n_bootstrap: int = 5000,
    confidence_level: float = 0.95,
    method: Literal['percentile', 'bca', 'basic'] = 'percentile',
    random_state: Optional[int] = None
) -> StatisticalResult
Parameters
data np.ndarray Required
Input data array to resample from.
statistic Callable[[np.ndarray], float] Required
Function that computes the statistic of interest from a data array.
n_bootstrap int Optional 5000
Number of bootstrap resamples to draw.
confidence_level float Optional 0.95
Confidence level for the interval (0.0–1.0).
method str Optional 'percentile'
Bootstrap method:
  • 'percentile' — Standard percentile method
  • 'bca' — Bias-corrected and accelerated (more accurate)
  • 'basic' — Basic bootstrap
random_state Optional[int] Optional None
Random seed for reproducibility.
Returns

StatisticalResult — Contains point_estimate, lower_bound, upper_bound, confidence_level, method, n_samples, and interval_type.

bayesian_proportion_ci

Compute Bayesian credible interval for a proportion using a Beta-Binomial conjugate model. Recommended for small samples (<30).

python
bayesian_proportion_ci(
    successes: int,
    trials: int,
    confidence_level: float = 0.95,
    prior_alpha: float = 1.0,
    prior_beta: float = 1.0
) -> StatisticalResult
Parameters
successes int Required
Number of successes (e.g., positive predictions in a group).
trials int Required
Total number of trials (e.g., total group size).
confidence_level float Optional 0.95
Credible interval width (0.0–1.0).
prior_alpha float Optional 1.0
Beta prior α parameter. 1.0 = uniform (non-informative) prior.
prior_beta float Optional 1.0
Beta prior β parameter. 1.0 = uniform (non-informative) prior.
Returns

StatisticalResult — Contains point_estimate (posterior mean), lower_bound, upper_bound, and interval_type='credible'.

stratified_bootstrap_ci v0.0.8

Stratified bootstrap CI that maintains group proportions during resampling. Essential for fairness metrics where the group structure must be preserved.

python
stratified_bootstrap_ci(
    data: np.ndarray,
    groups: np.ndarray,
    statistic: Callable[[np.ndarray, np.ndarray], float],
    n_bootstrap: int = 5000,
    confidence_level: float = 0.95,
    method: Literal['percentile', 'bca', 'basic'] = 'percentile',
    random_state: Optional[int] = None
) -> StatisticalResult

Used internally by compute_metric_with_ci() for fairness metrics. The statistic function receives (data, groups) and must return a float.

bayesian_difference_ci v0.0.8

Bayesian credible interval for the difference between two proportions via Monte Carlo sampling from Beta posterior distributions. Used in intersectional analysis for small-sample subgroup comparisons.

python
bayesian_difference_ci(
    successes1: int, trials1: int,
    successes2: int, trials2: int,
    confidence_level: float = 0.95,
    prior_alpha: float = 1.0,
    prior_beta: float = 1.0,
    n_samples: int = 10000,
    random_state: Optional[int] = None
) -> StatisticalResult  # point_estimate = p1 - p2
bayesian_mean_ci v0.0.8

Bayesian credible interval for a continuous mean using a Normal-Normal conjugate prior. Weakly informative prior with default prior_std=10.

python
bayesian_mean_ci(
    data: np.ndarray,
    confidence_level: float = 0.95,
    prior_mean: float = 0.0,
    prior_std: float = 10.0,
    n_samples: int = 10000,
    random_state: Optional[int] = None
) -> StatisticalResult
compute_metric_with_ci v0.0.8

High-level interface for computing any fairness metric with confidence intervals. Auto-selects the appropriate method (bootstrap or Bayesian) based on the minimum group size.

python
compute_metric_with_ci(
    metric_func: Callable,
    y_true: np.ndarray,
    y_pred: np.ndarray,
    sensitive_attr: np.ndarray,
    n_bootstrap: int = 5000,
    confidence_level: float = 0.95,
    method: Literal['auto', 'bootstrap', 'bayesian'] = 'auto',
    random_state: Optional[int] = None,
    **metric_kwargs
) -> StatisticalResult

When method='auto': uses Bayesian for minimum group size < 30, enhanced bootstrap for 30-50, and standard bootstrap for > 50. Performs stratified bootstrap to maintain group proportions.

Hypothesis Testing

permutation_test

Gold-standard permutation test for determining whether an observed fairness disparity is statistically significant.

python
permutation_test(
    y_pred: np.ndarray,
    sensitive_attr: np.ndarray,
    metric_func: Callable[[np.ndarray, np.ndarray], float],
    n_permutations: int = 10000,
    alternative: str = 'two-sided',
    random_state: Optional[int] = None
) -> PermutationTestResult
Parameters
y_pred np.ndarray Required
Model predictions.
sensitive_attr np.ndarray Required
Protected attribute defining groups.
metric_func Callable Required
Fairness metric function with signature (y_pred, sensitive_attr) -> float.
n_permutations int Optional 10000
Number of random permutations to generate the null distribution.
alternative str Optional 'two-sided'
Alternative hypothesis: 'two-sided', 'greater', or 'less'.
random_state Optional[int] Optional None
Random seed for reproducibility.
Returns

PermutationTestResult — Contains observed_statistic, p_value, null_distribution, n_permutations, significant_at_05, significant_at_01, and alternative.

permutation_test_demographic_parity

Convenience wrapper for permutation test on demographic parity difference.

python
permutation_test_demographic_parity(
    y_pred: np.ndarray,
    sensitive_attr: np.ndarray,
    n_permutations: int = 10000,
    random_state: Optional[int] = None
) -> PermutationTestResult
Parameters
y_pred np.ndarray Required
Model predictions (0/1).
sensitive_attr np.ndarray Required
Protected attribute defining groups.
n_permutations int Optional 10000
Number of random permutations.
random_state Optional[int] Optional None
Random seed for reproducibility.
Returns

PermutationTestResult — Same structure as permutation_test(), pre-configured for demographic parity difference.

contingency_test

Chi-square or Fisher’s exact test for independence between predictions and a protected attribute.

python
contingency_test(
    y_pred: np.ndarray,
    sensitive_attr: np.ndarray,
    min_expected: float = 5.0
) -> ContingencyTestResult
Parameters
y_pred np.ndarray Required
Model predictions (categorical/binary).
sensitive_attr np.ndarray Required
Protected attribute defining groups.
min_expected float Optional 5.0
Minimum expected cell count. Falls back to Fisher’s exact test if any expected count is below this threshold.
Returns

ContingencyTestResult — Contains test_used ('chi_square' or 'fisher_exact'), statistic, p_value, cramers_v, contingency_table, significant_at_05, and degrees_of_freedom.

comprehensive_fairness_test

Run multiple statistical significance tests for fairness in a single call. Combines permutation tests, contingency tests, and effect sizes with Benjamini-Hochberg correction.

python
comprehensive_fairness_test(
    y_true: np.ndarray,
    y_pred: np.ndarray,
    sensitive_attr: np.ndarray,
    metrics: Optional[List[str]] = None,
    n_permutations: int = 5000,
    alpha: float = 0.05,
    random_state: Optional[int] = None
) -> Dict[str, Any]
Parameters
y_true np.ndarray Required
True binary labels.
y_pred np.ndarray Required
Model predictions.
sensitive_attr np.ndarray Required
Protected attribute defining groups.
metrics Optional[List[str]] Optional None
List of metric names to test. Tests all available classification metrics if None.
n_permutations int Optional 5000
Number of permutations for each test.
alpha float Optional 0.05
Significance level. Applied after BH correction.
random_state Optional[int] Optional None
Random seed for reproducibility.
Returns

Dict[str, Any] — Contains per-metric results (p-value, corrected p-value, effect size, significance), contingency_test results, summary (number significant / total), and correction_method.

Robustness Testing

Tools for evaluating the stability and robustness of fairness metrics under data perturbations.

sensitivity_analysis

Analyze the sensitivity of a fairness metric to data perturbations (label noise, sampling, feature noise).

python
sensitivity_analysis(
    y_pred: np.ndarray,
    sensitive_attr: np.ndarray,
    metric_func: Callable[[np.ndarray, np.ndarray], float],
    perturbation_type: str = 'label_noise',
    perturbation_rate: float = 0.05,
    n_iterations: int = 100,
    robustness_threshold: float = 0.1,
    random_state: Optional[int] = None
) -> SensitivityResult
Parameters
y_pred np.ndarray Required
Model predictions.
sensitive_attr np.ndarray Required
Protected attribute array.
metric_func Callable Required
Fairness metric function with signature (y_pred, sensitive_attr) -> float.
perturbation_type str Optional 'label_noise'
Type of perturbation: 'label_noise', 'sampling', or 'feature_noise'.
perturbation_rate float Optional 0.05
Fraction of data to perturb in each iteration.
n_iterations int Optional 100
Number of perturbation iterations to run.
robustness_threshold float Optional 0.1
Maximum acceptable variation in metric before flagging as not robust.
random_state Optional[int] Optional None
Random seed for reproducibility.
Returns

SensitivityResult — Contains original_value, perturbed_values, mean_perturbed, std_perturbed, max_deviation, is_robust, perturbation_type, and robustness_threshold.

stress_test_fairness

Comprehensive stress testing of a fairness metric across multiple perturbation budgets.

python
stress_test_fairness(
    y_pred: np.ndarray,
    sensitive_attr: np.ndarray,
    metric_func: Callable[[np.ndarray, np.ndarray], float],
    perturbation_budgets: List[float] = [0.01, 0.05, 0.10],
    n_iterations: int = 100,
    random_state: Optional[int] = None
) -> Dict[str, Any]
Parameters
y_pred np.ndarray Required
Model predictions.
sensitive_attr np.ndarray Required
Protected attribute array.
metric_func Callable Required
Fairness metric function with signature (y_pred, sensitive_attr) -> float.
perturbation_budgets List[float] Optional [0.01, 0.05, 0.10]
List of perturbation rates to test. Each budget runs a full sensitivity sweep.
n_iterations int Optional 100
Number of iterations per budget level.
random_state Optional[int] Optional None
Random seed for reproducibility.
Returns

Dict[str, Any] — Contains original_value, per-budget results (mean, std, max deviation), and overall robustness_assessment.

subgroup_robustness_audit

Audit fairness across intersectional subgroups to detect “fairness gerrymandering” — where overall fairness hides subgroup disparities.

python
subgroup_robustness_audit(
    y_pred: np.ndarray,
    sensitive_attrs: Union[pd.DataFrame, Dict[str, np.ndarray]],
    y_true: Optional[np.ndarray] = None,
    min_subgroup_size: int = 30,
    disparity_threshold: float = 0.10,
    metric: str = 'positive_rate'
) -> SubgroupAuditResult
Parameters
y_pred np.ndarray Required
Model predictions.
sensitive_attrs Union[pd.DataFrame, Dict] Required
Multiple protected attributes as a DataFrame or dict of arrays. All intersections are automatically generated.
y_true Optional[np.ndarray] Optional None
True labels. Required for metrics like TPR; optional for positive_rate.
min_subgroup_size int Optional 30
Minimum subgroup size for inclusion in the audit.
disparity_threshold float Optional 0.10
Threshold for flagging a subgroup as having disparate outcomes.
metric str Optional 'positive_rate'
Metric to compute per subgroup (e.g., 'positive_rate', 'tpr', 'fpr').
Returns

SubgroupAuditResult — Contains subgroup_metrics, flagged_subgroups, worst_subgroup, best_subgroup, max_disparity, gerrymandering_detected, and n_subgroups_analyzed.

Approach

Robustness testing in vfairness is performed through bootstrap confidence intervals and permutation tests. Use *_with_ci metric variants (see Confidence Intervals) and permutation tests to assess whether observed disparities are robust or artifacts of sampling noise.

Proportion Tests & Effect Sizes

proportion_z_test

Two-proportion z-test for comparing rates between groups. Returns a two-sided p-value. No scipy dependency (uses error function approximation).

python
proportion_z_test(
    p1: float,    # Proportion in group 1
    n1: int,      # Sample size of group 1
    p2: float,    # Proportion in group 2
    n2: int       # Sample size of group 2
) -> float        # Two-sided p-value

Reference: Agresti & Caffo (2000). Simple and effective confidence intervals for proportions and differences of proportions.

fisher_exact_test

Fisher's exact test for a 2×2 contingency table. Computes exact hypergeometric probabilities for small samples (n ≤ 200), falls back to chi-squared approximation for larger tables. No scipy dependency.

python
fisher_exact_test(
    a: int, b: int,    # Row 1 counts
    c: int, d: int     # Row 2 counts
) -> float             # Two-sided p-value

Reference: Fisher (1922). On the interpretation of chi-square from contingency tables, and the calculation of P.

cohens_h

Cohen's h effect size for the difference between two proportions. The appropriate effect size measure for binary fairness outcomes (unlike Cohen's d which is for continuous data). Uses the arcsine transformation: h = 2 · arcsin(√p1) - 2 · arcsin(√p2).

python
cohens_h(p1: float, p2: float) -> float
|h|Interpretation
< 0.2Negligible
0.2 – 0.5Small
0.5 – 0.8Medium
≥ 0.8Large

Reference: Cohen (1988). Statistical Power Analysis for the Behavioral Sciences. Chapter 6.

cohens_h_interpretation

Returns a human-readable interpretation of a Cohen's h value: 'negligible', 'small', 'medium', or 'large'.

python
cohens_h_interpretation(h: float) -> str

Power Analysis & Minimum Detectable Effect

minimum_detectable_effect

Computes the minimum detectable effect (MDE) for a two-proportion z-test given sample sizes and desired power. Essential for intersectional analysis where subgroup sizes vary widely.

python
minimum_detectable_effect(
    n1: int,                     # Sample size of group 1
    n2: int,                     # Sample size of group 2
    alpha: float = 0.05,         # Significance level
    power: float = 0.80,         # Desired statistical power
    baseline_rate: float = 0.5   # Assumed baseline proportion
) -> float                       # Minimum detectable difference

Reference: Cohen (1988). Statistical Power Analysis for the Behavioral Sciences.

power_warning

Generates a human-readable warning when a subgroup has insufficient statistical power to detect meaningful disparities. Returns None if power is adequate (n ≥ 200).

python
power_warning(
    group_size: int,       # Size of the subgroup
    total_size: int,       # Total dataset size
    alpha: float = 0.05    # Significance level
) -> Optional[str]         # Warning string or None

Used by generate_structured_findings() to annotate intersectional findings with power limitations, preventing users from over-interpreting non-significant results from small subgroups.

Intersectional Disparity Analysis

Dual-lens functions for identifying the most and least advantaged intersectional subgroups with both prediction-rate and ground-truth-rate analysis. These functions power the Fairness Navigator's Feature Engineering step and produce structured findings suitable for compliance reporting.

identify_privileged_groups v0.0.8

Identify the most and least advantaged groups with dual-lens analysis. Computes prediction rates, ground truth rates, false positive rates, and prediction deltas per subgroup. Supports outcome polarity: when the positive label is unfavorable (e.g., recidivism), the group with the lowest positive prediction rate is considered most advantaged.

python
identify_privileged_groups(
    y_true: ArrayLike,
    y_pred: ArrayLike,
    sensitive_attr: ArrayLike,
    *,
    min_group_size: int = 30,
    missing_strategy: MissingStrategy = 'exclude',
    outcome_polarity: str = 'positive_favorable'
) -> Dict[str, Any]
Parameters
y_true ArrayLike Required
True binary labels (0/1).
y_pred ArrayLike Required
Model predictions (0/1).
sensitive_attr ArrayLike Required
Protected attribute values (can be intersectional compound groups, e.g., "Male_White").
min_group_size int Optional 30
Minimum samples required per group.
outcome_polarity str Optional 'positive_favorable'
'positive_favorable' (e.g., loan approved: highest rate = most advantaged) or 'positive_unfavorable' (e.g., recidivism predicted: lowest rate = most advantaged).
Returns

Dict[str, Any] containing:

KeyTypeDescription
privileged_groupGroupAdvantageMost advantaged group (highest or lowest rate depending on polarity)
disadvantaged_groupGroupAdvantageLeast advantaged group
all_groupsList[GroupAdvantage]All groups sorted by advantage (descending for favorable, ascending for unfavorable)
overall_ratefloatOverall positive prediction rate
overall_ground_truth_ratefloatOverall ground truth positive rate
max_disparityfloatMaximum disparity between any two groups (prediction rates)
max_ground_truth_disparityfloatMaximum disparity in ground truth rates (for comparison)
disparity_severitystrSeverity assessment: 'info', 'low', 'medium', 'high', 'critical'
outcome_polaritystrThe polarity used for this analysis
prediction_equals_targetboolWhether y_true and y_pred are identical (no separate prediction column)

GroupAdvantage Dataclass

FieldTypeDescription
groupstrGroup name/identifier
positive_ratefloatRate of positive predictions
ground_truth_ratefloatRate of positive ground truth labels
false_positive_ratefloatFPR: wrongly predicted positive among actual negatives
prediction_deltafloatpositive_rate minus ground_truth_rate (positive = over-prediction, negative = under-prediction)
sizeintNumber of samples
relative_to_overallfloatRatio compared to overall rate
relative_to_bestfloatRatio compared to best group
disparity_contributionfloatHow far from the average rate
severitystrSeverity based on gap from best group
Dual-Lens Analysis

By comparing max_disparity (prediction rates) against max_ground_truth_disparity (ground truth rates), you can determine whether the model is amplifying or mitigating societal disparities. A prediction disparity significantly larger than the ground truth disparity indicates the model is introducing additional bias.

intersectional_disparity_analysis v0.0.8

Comprehensive intersectional disparity analysis with dual-lens comparison. Computes both prediction and ground truth rates per subgroup, identifies where the model amplifies or mitigates societal disparities, and generates structured findings with severity ratings.

python
intersectional_disparity_analysis(
    y_true: ArrayLike,
    y_pred: ArrayLike,
    sensitive_attr: ArrayLike,
    *,
    single_attributes: Optional[Dict[str, ArrayLike]] = None,
    min_group_size: int = 30,
    missing_strategy: MissingStrategy = 'exclude',
    outcome_polarity: str = 'positive_favorable'
) -> Dict[str, Any]
Parameters
y_true, y_pred, sensitive_attr ArrayLike Required
Same as identify_privileged_groups().
single_attributes Optional[Dict[str, ArrayLike]] Optional
Individual attribute arrays for comparison, e.g., {'gender': gender_array, 'race': race_array}. Enables intersectional vs. single-attribute comparison.
outcome_polarity str Optional 'positive_favorable'
'positive_favorable' or 'positive_unfavorable'. Determines sorting and advantage direction.
Returns

Dict[str, Any] containing: intersectional_analysis (full dual-lens group analysis), single_attribute_analyses (per-attribute analysis if provided), comparison (intersectional vs. single-attribute comparison), insights (human-readable strings), recommendations (actionable items), and findings (structured findings with types, severities, and metric values).

Example

python
from vfairness.evaluation import intersectional_disparity_analysis

analysis = intersectional_disparity_analysis(
    y_true, y_pred,
    gender_race,   # e.g., "Male_White", "Female_Black"
    single_attributes={'gender': gender, 'race': race},
    outcome_polarity='positive_favorable'
)

# Dual-lens comparison
inter = analysis['intersectional_analysis']
print(f"Most advantaged: {inter['privileged_group'].group}")
print(f"  Prediction rate: {inter['privileged_group'].positive_rate:.1%}")
print(f"  Ground truth:    {inter['privileged_group'].ground_truth_rate:.1%}")
print(f"  Over-prediction: {inter['privileged_group'].prediction_delta:+.1%}")
print(f"Max disparity (prediction): {inter['max_disparity']:.1%}")
print(f"Max disparity (ground truth): {inter['max_ground_truth_disparity']:.1%}")

# Structured findings with severity
for finding in analysis['findings']:
    print(f"[{finding['severity']}] {finding['description']}")
intersectional_disparity_to_svg v0.0.8 SVG

Render an intersectional disparity analysis result as a ranked-bar SVG chart. Shows most/least advantaged groups, ground truth overlays, severity-colored bars, prediction deltas, and key insights. Follows the vfairness G1 Design System.

python
from vfairness.rendering import intersectional_disparity_to_svg

svg = intersectional_disparity_to_svg(
    result,                     # Output of intersectional_disparity_analysis()
    explanation="Custom note",  # Optional explanation panel text
    save_path="disparity.svg"   # Optional file path
)
# Returns SVG markup string

Accepts both camelCase and snake_case input keys for flexibility. The chart includes: comparison cards for privileged vs. disadvantaged groups, a disparity arrow with severity color, ranked bars sorted by positive rate (with ground truth overlay and delta indicators), and up to 6 insight cards colored by finding severity.

Visualization

Fairness Visualization

Fairness Metrics
Bar chart of all fairness metrics with threshold lines
plot_fairness_metrics()
Matplotlib
Group Comparison
Side-by-side group statistics comparison
plot_group_comparison()
Matplotlib
Confidence Intervals
CI visualization for each metric with error bars
plot_confidence_intervals()
Matplotlib
Effect Sizes
Effect size visualization across metrics
plot_effect_sizes()
Matplotlib
Metrics Radar
Radar / spider chart of multiple metrics at once
plot_metrics_radar()
Matplotlib
Disparity Heatmap
Pairwise group disparity heatmap
plot_group_disparity_heatmap()
Matplotlib
Fairness Report
Multi-panel comprehensive figure — publication-ready
plot_fairness_report()
Matplotlib
Interactive Dashboard
Full interactive Plotly dashboard with hover & zoom
create_fairness_dashboard()
Plotly
Save All Plots
Batch-save all plots to a directory at once
save_fairness_plots()
Files on disk

Available Styles

python
from vfairness import plot_fairness_report, get_available_styles

# Available styles
print(get_available_styles())
# ['academic', 'business', 'modern', 'dark']

# Generate publication-ready figure
fig = plot_fairness_report(
    report,
    style='academic',
    figsize=(12, 8)
)
fig.savefig('fairness_report.pdf', dpi=300, bbox_inches='tight')

SVG Templates vfairness.rendering

Lightweight SVG alternatives to the Matplotlib/Plotly charts above, generated via Jinja2 templates (3–6 KB each). All adapters accept a FairnessAnalyzer.get_report() dict and an optional save_path.

View all evaluation templates in the SVG Gallery →

python
from vfairness.rendering import radar_chart_to_svg, metrics_bar_chart_to_svg

report = analyzer.get_report()
svg = radar_chart_to_svg(report, save_path="radar.svg")
svg = metrics_bar_chart_to_svg(report, save_path="metrics.svg")
About the SVG rendering engine

All *_to_svg() adapters across this page use the vfairness.rendering module — a Jinja2-powered template engine with 49 SVG templates and custom filters (pct, f2, risk_color, etc.). Use render_svg(template, data) for low-level access or list_templates() to discover all available templates.

5. Operations & Monitoring

CI/CD integration, MLOps, and production monitoring for fairness validation in deployment pipelines.

python
from vfairness.operations import (
    DataBiasValidator,
    ModelFairnessGate,
    FairnessTestSuite
)

What's Included

CI/CD Integration
DataBiasValidator and ModelFairnessGate for pipeline validation
Testing
FairnessTestSuite and pytest integration for test-driven fairness
MLOps
MLflow integration for tracking fairness metrics in experiments
Monitoring
BiasMonitor for production drift detection and alerting
flowchart LR subgraph OPS["Operations & Monitoring"] direction TB DATA[Data Pipeline] --> DBV[DataBiasValidator] DBV --> TRAIN[Train Model] TRAIN --> TEST[FairnessTestSuite] TEST --> GATE[ModelFairnessGate] GATE -->|Approved| DEPLOY[Deploy] DEPLOY --> MON[BiasMonitor] MON -->|Drift| ALERT[Alert] end subgraph CICD["CI/CD"] DBV2[Data Validation] MFG[Deployment Gate] FTS[Test Suite] end subgraph Monitoring["Monitoring"] BM[BiasMonitor] DD[Drift Detection] ALT[Alerting] end GATE --> CICD MON --> Monitoring style GATE fill:#059669,stroke:#333,stroke-width:2px,color:#fff style OPS fill:#d1fae5,stroke:#059669 style CICD fill:#ecfdf5,stroke:#059669 style Monitoring fill:#ecfdf5,stroke:#059669 style DEPLOY fill:#d1fae5,stroke:#10b981 style ALERT fill:#fee2e2,stroke:#ef4444
Click diagram to zoom & pan

CI/CD Integration

CI/CD Integration

Automated fairness validation for continuous integration and deployment pipelines.

Demo Notebook: vfairness_8_cicd_demo.ipynb — Pipeline validators, fairness gates & bias monitoring
Module Import
from vfairness.operations.cicd import DataBiasValidator, ModelFairnessGate, FairnessTestSuite, BiasMonitor
flowchart LR subgraph Pipeline["CI/CD Pipeline"] direction TB A[Data Pipeline] --> B[DataBiasValidator] B --> C{Validation
Passed?} C -->|Yes| D[Model Training] C -->|No| E[Block & Alert] D --> F[FairnessTestSuite] F --> G{Tests
Passed?} G -->|Yes| H[ModelFairnessGate] G -->|No| I[Block & Alert] H --> J{Gate
Approved?} J -->|Yes| K[Deploy Model] J -->|No| L[Block & Alert] K --> M[BiasMonitor] M --> N{Drift
Detected?} N -->|Yes| O[Trigger Alert] N -->|No| M end style A fill:#e0e7ff,stroke:#6366f1 style B fill:#fef3c7,stroke:#f59e0b style D fill:#e0e7ff,stroke:#6366f1 style F fill:#fef3c7,stroke:#f59e0b style H fill:#fef3c7,stroke:#f59e0b style K fill:#d1fae5,stroke:#10b981 style M fill:#fef3c7,stroke:#f59e0b style E fill:#fee2e2,stroke:#ef4444 style I fill:#fee2e2,stroke:#ef4444 style L fill:#fee2e2,stroke:#ef4444 style O fill:#fee2e2,stroke:#ef4444
Click diagram to zoom & pan

Key Components

DataBiasValidator
Validates data pipelines for bias before model training Data Ingestion
ModelFairnessGate
Deployment gates that enforce fairness requirements Pre-Deployment
FairnessTestSuite
pytest integration for test-driven bias prevention Testing
BiasMonitor
Continuous monitoring with drift detection and alerting Production

DataBiasValidator

Validates data pipelines for bias issues before model training begins.

Background

DataBiasValidator is designed for automated CI/CD pipelines where data arrives on a schedule and must be validated before being consumed by training jobs. Unlike BiasDetector (which produces an exploratory audit report), DataBiasValidator returns a strict pass/fail decision with configurable thresholds, making it suitable for pipeline orchestrators like Airflow, Prefect, or GitHub Actions.

The validator checks five dimensions: demographic representation (are groups large enough?), outcome disparity (are label rates unacceptably skewed?), missing values (is data quality degraded for some groups?), label balance, and general data hygiene (total-sample adequacy, duplicate rows, zero-variance columns). Results export to JUnit XML for direct integration with CI dashboards.

Data hygiene = the "fourth dimension" of data validation

Following the Turing AI Fairness curriculum (Module 4): "Traditional data validation focuses on completeness, accuracy, and consistency. We add a crucial fourth dimension: equity." Fairness conclusions are only as trustworthy as the structural integrity of the data they rest on, so DataBiasValidator now also validates total-sample adequacy, duplicate rows, and constant (zero-variance) columns as part of the canonical contract — rather than leaving these to each consumer to re-implement. Pulse (the quick-assessment surface) is a pure translator of this result and runs no checks of its own.

DataBiasValidator vs BiasDetector

BiasDetector is an exploratory tool for data scientists (rich reports, risk scores, 43 historical patterns). DataBiasValidator is an automated gate for pipelines (pass/fail, JUnit XML, configurable thresholds). Use both: BiasDetector during development, DataBiasValidator in production pipelines.

Constructor

python
DataBiasValidator(
    protected_attributes: List[str],           # Columns to check for bias
    representation_thresholds: Dict = None,    # min_group_fraction, min_samples_per_group
    disparity_thresholds: Dict = None,         # max_outcome_ratio
    config: DataValidationConfig = None        # Full configuration object
)

Configuration Options

Parameter Default Description
min_group_fraction 0.05 Minimum fraction of data each group should represent
max_outcome_ratio 2.0 Maximum allowed ratio between group outcome rates
min_samples_per_group 30 Minimum samples required per demographic group
missing_value_threshold 0.1 Maximum allowed fraction of missing values
fail_on_warning False Whether to fail validation on warnings
check_data_hygiene True Run general data-hygiene checks (total-sample adequacy, duplicate rows, zero-variance columns)
min_total_samples 200 Below this row count, estimates are flagged as indicative (CIs too wide to be conclusive)
max_duplicate_fraction 0.05 Duplicate-row fraction above which the dataset is flagged as an error (duplicates silently reweight groups)

Issue types

Each ValidationIssue carries a stable issue_type. In addition to the representation / disparity / missing-pattern / label-quality types, the data-hygiene dimension emits:

issue_typeSeverityMeaning
insufficient_total_samplesWARNINGFewer than min_total_samples rows; fairness estimates are indicative only
duplicate_rowsERROR / WARNINGDuplicate rows above (ERROR) or below (WARNING) max_duplicate_fraction
constant_columnsWARNINGOne or more columns hold a single constant value (no signal)

result.metrics['data_hygiene'] additionally exposes n_samples, n_duplicate_rows, duplicate_fraction, and constant_columns for dashboards.

Example Usage

python
from vfairness.operations.cicd import DataBiasValidator

# Create validator with custom thresholds
validator = DataBiasValidator(
    protected_attributes=['gender', 'race'],
    representation_thresholds={'min_group_fraction': 0.05},
    disparity_thresholds={'max_outcome_ratio': 2.0}
)

# Validate data pipeline
result = validator.validate(df, outcome_column='approved')

if not result.passed:
    print(f"Validation failed: {result.summary}")
    for issue in result.errors:
        print(f"  - [{issue.severity.value}] {issue.message}")
        if issue.recommendation:
            print(f"    Recommendation: {issue.recommendation}")
    # In CI/CD, you might raise an exception here
    raise ValueError(f"Data bias detected: {result.summary}")

# Export to JUnit XML for CI/CD reporting
junit_xml = result.to_junit_xml()
with open('data_validation_results.xml', 'w') as f:
    f.write(junit_xml)

DataValidationResult

Property Type Description
passed bool Whether validation passed all checks
issues List[ValidationIssue] All validation issues found
errors List[ValidationIssue] Only ERROR and CRITICAL issues
warnings List[ValidationIssue] Only WARNING level issues
metrics Dict Computed validation metrics
summary str Human-readable summary

ModelFairnessGate

Deployment gate that enforces fairness requirements before model deployment.

Background

ModelFairnessGate is the final checkpoint before a trained model reaches production. It evaluates a configurable set of fairness metrics against explicit thresholds and returns a APPROVED, BLOCKED, or CONDITIONAL decision. When require_improvement=True, the gate also verifies that fairness has improved over a baseline model, preventing regressions across retraining cycles.

Gate decisions include structured blocking_reasons and can be exported as GitHub Check Run payloads or Markdown PR comments, making fairness status visible in code review workflows.

Constructor

python
ModelFairnessGate(
    metrics: List[str] = None,                 # Metrics to evaluate
    thresholds: Dict[str, float] = None,       # Threshold for each metric
    require_improvement: bool = False,          # Require improvement over baseline
    improvement_margin: float = 0.0,            # Minimum improvement required
    blocking_metrics: List[str] = None,         # Metrics that block deployment
    config: GateConfig = None                   # Full configuration object
)

Supported Metrics

  • demographic_parity_difference
  • equalized_odds_difference
  • false_positive_rate_difference
  • predictive_parity_difference

Example Usage

python
from vfairness.operations.cicd import ModelFairnessGate

# Create fairness gate
gate = ModelFairnessGate(
    metrics=['demographic_parity_difference', 'equalized_odds_difference'],
    thresholds={
        'demographic_parity_difference': 0.1,
        'equalized_odds_difference': 0.15
    },
    require_improvement=True  # Require improvement over baseline
)

# Evaluate model
decision = gate.evaluate(
    y_true=y_test,
    y_pred=model.predict(X_test),
    protected_attr=gender_test,
    baseline_metrics={'demographic_parity_difference': 0.12}
)

if decision.approved:
    print(f"Model APPROVED: {decision.summary}")
    deploy_model()
else:
    print(f"Model BLOCKED: {decision.summary}")
    for reason in decision.blocking_reasons:
        print(f"  - {reason}")

# Generate markdown report (useful for PR comments)
print(decision.to_markdown_report())

# Create GitHub Check Run payload
github_check = gate.create_github_check(decision)

GateDecision

Property Type Description
approved bool Whether model is approved for deployment
status GateStatus APPROVED, BLOCKED, or CONDITIONAL
metric_evaluations List[MetricEvaluation] Evaluation results for each metric
blocking_reasons List[str] Reasons why deployment was blocked
warnings List[str] Warning messages

Hierarchical Gate

Multi-level fairness evaluation that catches harm hidden in intersectional subgroups.

Background

A model that passes fairness checks at the overall level may still discriminate against specific intersections (e.g., older women, young Black men). evaluate_hierarchical() addresses this by evaluating fairness at three nested levels: overall (single protected attribute), single-attribute (each attribute independently), and intersectional (all attribute combinations up to a configurable depth). Per-intersection threshold multipliers allow stricter standards for vulnerable subgroups.

Constructor

python
HierarchicalGateConfig(
    check_overall: bool = True,              # Check overall (first attribute)
    check_single_attributes: bool = True,    # Check each attribute separately
    check_intersections: bool = True,        # Check intersectional combos
    intersection_depth: int = 2,             # Max attributes per intersection
    per_intersection_thresholds: Dict = None,# Custom thresholds per group
    default_intersection_threshold_multiplier: float = 1.2,
    min_group_size: int = 30                 # Minimum samples for reliable results
)

Example Usage

python
from vfairness.operations.cicd import ModelFairnessGate, HierarchicalGateConfig

gate = ModelFairnessGate(
    metrics=['demographic_parity_difference', 'equal_opportunity_difference'],
    thresholds={'demographic_parity_difference': 0.1, 'equal_opportunity_difference': 0.1}
)

config = HierarchicalGateConfig(
    check_intersections=True,
    intersection_depth=2,
    per_intersection_thresholds={
        'gender_Female&race_Black': {'demographic_parity_difference': 0.08}
    }
)

decision = gate.evaluate_hierarchical(
    y_true, y_pred,
    protected_attributes=['gender', 'race'],
    hierarchical_config=config
)

# Three-level result
for level, result in decision.level_results.items():
    print(f"{level}: {'APPROVED' if result.approved else 'BLOCKED'}")

# Small-sample warnings
for w in decision.small_sample_warnings:
    print(f"Warning: {w.group_name} has only {w.sample_size} samples")

IntersectionalGateDecision

PropertyTypeDescription
approved bool True only if all levels pass
level_results Dict[str, GateDecision] Per-level gate decisions (overall, single_attribute, intersectional)
small_sample_warnings List[SmallSampleWarning] Warnings for groups below min_group_size

FairnessReportCard

Generates structured, PR-ready fairness reports from gate decisions.

Background

Code review is a natural enforcement point for fairness. FairnessReportCard transforms a GateDecision or IntersectionalGateDecision into a structured markdown report that can be posted as a PR comment. Reviewers see pass/fail badges, a metric table, intersectional breakdowns, and small-sample warnings — all without leaving the pull request. The card also includes a machine-readable payload for the GitHub API.

Constructor

python
FairnessReportCard(
    gate_decision: Union[GateDecision, IntersectionalGateDecision],
    model_name: Optional[str] = None,
    include_intersectional: bool = True
)

Methods

MethodReturnsDescription
to_markdown() str Full markdown report with pass/fail badges, metric table, intersectional breakdown, and recommendations
to_github_comment_payload() Dict Ready-to-POST payload for the GitHub Issues/PR Comments API

Example Usage

python
from vfairness.operations.cicd import FairnessReportCard

card = FairnessReportCard(decision, model_name='loan-approval-v2.1')

# Markdown for PR comment
markdown = card.to_markdown()
print(markdown)

# GitHub API payload
payload = card.to_github_comment_payload()
# POST to /repos/{owner}/{repo}/issues/{pr_number}/comments

FairnessTestSuite

pytest-compatible test suite for fairness validation in CI/CD pipelines.

Background

FairnessTestSuite makes fairness requirements executable specifications. Instead of manually checking metric values after each training run, you write fairness tests alongside unit tests. If the model's demographic parity difference exceeds 0.10, the test fails and the CI build breaks — just like a failing unit test.

The suite integrates natively with pytest via fixtures, decorators (@fairness_test), and assertion functions (assert_fairness). Results export to JUnit XML for CI dashboards, and a companion ModelFairnessGate can block deployment when critical tests fail.

CI/CD Pipeline Integration

The diagram below shows how DataBiasValidator, FairnessTestSuite, and ModelFairnessGate form a three-stage fairness gate within a CI/CD pipeline.

Constructor

python
FairnessTestSuite(
    protected_attributes: List[str],           # Attributes to test
    metrics: List[str] = None,                 # Metrics to test
    thresholds: Dict[str, float] = None        # Thresholds for each metric
)

pytest Integration

python
# conftest.py
import pytest
from vfairness.operations.cicd import FairnessTestSuite

@pytest.fixture
def fairness_suite():
    return FairnessTestSuite(
        protected_attributes=['gender'],
        metrics=['demographic_parity_difference'],
        thresholds={'demographic_parity_difference': 0.1}
    )

# test_fairness.py
def test_model_fairness(fairness_suite, trained_model, test_data):
    """Test that model meets fairness requirements."""
    results = fairness_suite.test_model(
        model=trained_model,
        test_data=test_data,
        target_column='target',
        raise_on_failure=True  # Raises FairnessAssertionError if tests fail
    )

    # All tests passed if we reach here
    summary = fairness_suite.get_summary()
    assert summary['status'] == 'passed'

Using Decorators

python
from vfairness.operations.cicd import fairness_test, assert_fairness

# Using decorator
@fairness_test(
    metric='demographic_parity_difference',
    threshold=0.1,
    protected_attribute='gender'
)
def test_loan_model_fairness(model, test_data):
    """Returns (y_true, y_pred, protected_attr) tuple."""
    y_pred = model.predict(test_data.drop('target', axis=1))
    return test_data['target'], y_pred, test_data['gender']

# Using assert function
def test_model_demographic_parity():
    y_pred = model.predict(X_test)

    assert_fairness(
        y_true=y_test,
        y_pred=y_pred,
        protected_attr=gender_test,
        metric='demographic_parity_difference',
        threshold=0.1,
        message="Model fails demographic parity requirement"
    )

JUnit XML Export

python
# Export results for CI/CD reporting
junit_xml = fairness_suite.to_junit_xml()
with open('fairness_test_results.xml', 'w') as f:
    f.write(junit_xml)

pytest Integration

assert_fairness

Assert that fairness metrics meet specified thresholds. Raises FairnessAssertionError if thresholds are exceeded.

python
assert_fairness(
    y_true: ArrayLike,
    y_pred: ArrayLike,
    sensitive_attr: ArrayLike,
    metrics: Optional[List[str]] = None,
    thresholds: Optional[Dict[str, float]] = None,
    message: Optional[str] = None
) -> Dict[str, float]
Parameters
y_true ArrayLike Required
True binary labels (0/1).
y_pred ArrayLike Required
Model predictions (0/1).
sensitive_attr ArrayLike Required
Protected attribute defining groups.
metrics List[str] Optional all metrics
List of metric names to assert. Uses all available metrics if omitted.
thresholds Dict[str, float] Optional standard defaults
Pass/fail threshold per metric. Raises FairnessAssertionError if any metric exceeds its threshold.
message str Optional None
Custom error message to include when assertion fails.
Returns

Dict[str, float] — Dictionary of computed metric values if all assertions pass.

python
from vfairness import assert_fairness, FairnessAssertionError
import pytest

def test_model_fairness():
    """Test that model meets fairness requirements."""
    y_true, y_pred, gender = get_test_data()

    # Will raise FairnessAssertionError if thresholds exceeded
    metrics = assert_fairness(
        y_true, y_pred, gender,
        metrics=['demographic_parity_difference', 'equal_opportunity_difference'],
        thresholds={
            'demographic_parity_difference': 0.1,
            'equal_opportunity_difference': 0.1
        },
        message="Model failed fairness requirements"
    )

    # If we reach here, all metrics passed
    assert metrics['demographic_parity_difference'] < 0.1


def test_fairness_with_expected_failure():
    """Test fairness with expected threshold violation."""
    y_true, y_pred, biased_attr = get_biased_data()

    with pytest.raises(FairnessAssertionError) as exc_info:
        assert_fairness(
            y_true, y_pred, biased_attr,
            thresholds={'demographic_parity_difference': 0.01}
        )

    # Inspect the exception
    assert 'demographic_parity_difference' in exc_info.value.failed_metrics

Pre-commit Hooks

Prevent fairness documentation gaps at the earliest possible moment — before code is even committed.

Background

Missing fairness documentation is one of the most common audit failures. vfairness ships two pre-commit hooks that validate fairness configuration files and model cards before code reaches the repository. This shift-left approach catches documentation gaps during local development rather than in code review or post-deployment audits.

Available Hooks

Hook IDWhat It Checks
vfairness-check-config Verifies that fairness configuration files (YAML/JSON) exist and contain required keys: protected_attributes, metrics, and thresholds
vfairness-check-model-card Checks that model card files include a fairness section with required subsections: Metrics, Limitations, and Testing

Setup

yaml
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/validantai/vfairness
    rev: v0.0.8
    hooks:
      - id: vfairness-check-config
        files: 'fairness[_-]config\.(ya?ml|json)$'
      - id: vfairness-check-model-card
        files: 'model[_-]card\.md$'

CLI Usage

bash
# Check fairness config
vfairness-precommit check-config fairness_config.yaml

# Check model card
vfairness-precommit check-model-card model_card.md

MLOps

MLflow Integration

log_fairness_to_mlflow

Log fairness metrics to an MLflow run.

python
from vfairness import FairnessAnalyzer, log_fairness_to_mlflow
import mlflow

analyzer = FairnessAnalyzer(y_true, y_pred, gender)

with mlflow.start_run():
    # Train your model...

    # Log fairness metrics
    log_fairness_to_mlflow(
        analyzer,
        prefix='fairness',
        log_params=True,      # Log min_group_size, etc.
        log_artifacts=True    # Save full report as JSON artifact
    )

# Logged metrics: fairness.demographic_parity_difference, etc.
# Logged params: fairness.min_group_size, fairness.task_type
# Logged artifacts: fairness_report.json
Parameters
analyzer FairnessAnalyzer Required
An initialized FairnessAnalyzer instance.
prefix str Optional 'fairness'
Prefix for logged metric names (e.g., fairness.demographic_parity_difference).
log_params bool Optional True
Log configuration parameters (min_group_size, task_type, etc.).
log_artifacts bool Optional True
Save the full fairness report as a JSON artifact.
Returns

Dict[str, float] — Dictionary of the fairness metrics that were logged to MLflow.

create_fairness_callback

Create a callback function for use with training frameworks. The callback evaluates fairness metrics at each training epoch/step.

python
create_fairness_callback(
    sensitive_attr_column: str,
    metrics: Optional[List[str]] = None,
    thresholds: Optional[Dict[str, float]] = None,
    fail_on_violation: bool = False
) -> Callable
Parameters
sensitive_attr_column str Required
Name of the protected attribute column to monitor during training.
metrics Optional[List[str]] Optional None
List of fairness metrics to evaluate. Uses default classification metrics if None.
thresholds Optional[Dict[str, float]] Optional None
Custom pass/fail thresholds per metric. Uses library defaults if omitted.
fail_on_violation bool Optional False
Whether to raise an exception if fairness thresholds are violated during training.
Returns

Callable — A callback function compatible with training loops. Accepts (y_true, y_pred, sensitive_attr) and returns a dict of metric values.

W&B Integration

Log fairness metrics alongside accuracy metrics in Weights & Biases for cross-version comparison and regression detection.

log_fairness_to_wandb

Log fairness metrics to a Weights & Biases run.

python
from vfairness import FairnessAnalyzer, log_fairness_to_wandb
import wandb

analyzer = FairnessAnalyzer(y_true, y_pred, gender)

with wandb.init(project='my-model'):
    log_fairness_to_wandb(
        analyzer,
        prefix='fairness',
        log_artifacts=True,       # Save full report as W&B artifact
        include_group_stats=True  # Include per-group statistics
    )
Parameters
analyzer_or_report Union[FairnessAnalyzer, Dict] Required
A FairnessAnalyzer instance or a report dict.
prefix str Optional 'fairness'
Prefix for logged metric names.
log_artifacts bool Optional True
Save the full fairness report as a W&B artifact.
include_group_stats bool Optional True
Include per-group statistics in logged metrics.
Returns

Dict[str, Any] — Dictionary of the fairness metrics that were logged to W&B.

Auto-Log Decorator

Automatically log fairness metrics from any training function with zero code changes inside the function body.

@auto_log_fairness

Decorator that wraps a training function and automatically logs fairness metrics to the active experiment tracker (MLflow or W&B).

python
@auto_log_fairness(
    backend: str = 'mlflow',          # 'mlflow' or 'wandb'
    metrics: List[str] = None,        # Fairness metrics to compute
    thresholds: Dict[str, float] = None,
    protected_attr_column: str = None # Column name in input data
)

The decorated function should accept (X, y, sensitive_attr) or (X, y) and return predictions (array-like) or a fitted model with a .predict() method. The decorator intercepts the return value, computes fairness metrics, and logs them to the active run.

python
from vfairness import auto_log_fairness
import mlflow

@auto_log_fairness(backend='mlflow', metrics=['demographic_parity_difference'])
def train_model(X, y, sensitive):
    from sklearn.linear_model import LogisticRegression
    model = LogisticRegression().fit(X, y)
    return model.predict(X)

with mlflow.start_run():
    preds = train_model(X_train, y_train, gender)
    # fairness metrics are automatically logged to the active MLflow run

Production Monitoring

Demo Notebook: vfairness_5_monitoring_demo.ipynb — Real-time fairness tracking, drift detection & adaptive alerts

BiasMonitor

Continuous monitoring for fairness drift detection in production systems. BiasMonitor tracks fairness metrics over successive prediction batches and compares them against baseline values established during training or validation. When a metric deviates from its baseline beyond a configurable threshold, the monitor fires a severity-graded alert and optionally invokes a user-supplied callback (e.g. Slack, PagerDuty, email).

What Drift Is Monitored

BiasMonitor detects three categories of drift:

Drift TypeWhat It Captures
METRIC_DRIFT A tracked fairness metric (e.g. demographic parity difference) moves away from its baseline value beyond the configured drift_threshold. This is the primary detection mode.
DISTRIBUTION_DRIFT The composition of protected groups in incoming batches shifts relative to earlier batches. Enabled via enable_distribution_monitoring. Useful for catching upstream data-pipeline changes that alter group proportions.
PERFORMANCE_DRIFT Overall model performance degrades in a way that disproportionately affects certain groups. Enabled via enable_performance_monitoring.

Tracked KPIs

By default the monitor computes two core fairness KPIs per batch. Additional metrics can be supplied through a custom compute_metrics_fn.

MetricFormulaInterpretation
demographic_parity_difference P(ŷ=1 | G=0) − P(ŷ=1 | G=1) Difference in positive prediction rates between two groups. Values near 0 indicate parity; large absolute values indicate disparity.
equalized_odds_difference TPRG=0 − TPRG=1 Difference in true positive rates between groups (computed only when ground-truth labels y_true are available). Measures whether the model is equally accurate for positive cases across groups.

How Drift Detection Works

  1. Baseline establishment — At initialisation you provide baseline_metrics, typically the metric values measured on the training or validation set.
  2. Batch ingestion — Each call to log_batch() computes the configured metrics for the incoming batch and appends the values to a rolling window of size window_size.
  3. Minimum-sample guard — Alerts are suppressed until the monitor has accumulated at least min_samples_for_alert samples (default 100), avoiding noisy signals on small early batches.
  4. Absolute-deviation test — For each monitored metric the absolute difference |current_value − baseline_value| is compared against drift_threshold. A drift is flagged when the deviation exceeds the threshold.
  5. Severity grading — Alerts are assigned a severity level based on the magnitude of the deviation relative to the threshold:
    • INFO — deviation > 1× threshold
    • WARNING — deviation > 1.5× threshold
    • CRITICAL — deviation > 2× threshold
  6. Cooldown — Repeated alerts for the same metric are suppressed for alert_cooldown_seconds (default 3600 s / 1 hour) to avoid alert fatigue.
  7. Callback dispatch — If an alert_callback is registered, it is invoked with a DriftAlert object containing the metric name, baseline, current value, drift magnitude, severity, and timestamp.

Constructor

python
BiasMonitor(
    baseline_metrics: Dict[str, float] = None,  # Baseline to compare against
    drift_threshold: float = 0.05,              # Threshold for drift detection
    window_size: int = 10,                      # Batches for rolling statistics
    alert_callback: Callable = None,            # Function called when drift detected
    config: MonitorConfig = None,               # Full configuration object
    compute_metrics_fn: Callable = None         # Custom metric computation function
)
Parameters
baseline_metrics Dict[str, float] Optional
Dictionary mapping metric names to their expected baseline values, typically captured during training or validation (e.g. {"demographic_parity_difference": 0.05}). Each batch is compared against these values to detect drift.
drift_threshold float Default 0.05
Absolute deviation from the baseline above which a metric is considered to have drifted. Also controls severity grading: >1.5× triggers WARNING, >2× triggers CRITICAL.
window_size int Default 10
Number of recent batches retained for computing rolling statistics via get_rolling_average(). Older batch values are evicted automatically.
alert_callback Callable[[DriftAlert], None] Optional
A function invoked each time a drift alert fires. Receives a DriftAlert object. Use this to integrate with external alerting systems (Slack, PagerDuty, email, etc.).
config MonitorConfig Optional
Full configuration object. When provided, overrides drift_threshold and window_size and exposes additional options such as min_samples_for_alert, alert_cooldown_seconds, and distribution/performance monitoring toggles.
compute_metrics_fn Callable Optional
Custom function with signature (y_true, y_pred, protected_attr) -> Dict[str, float]. Replaces the built-in metric computation, allowing you to monitor any fairness KPI your organization requires.

Configuration Options (MonitorConfig)

Parameter Default Description
drift_threshold 0.05 Absolute deviation from baseline above which drift is flagged. Also determines severity tiers (1× = INFO, 1.5× = WARNING, 2× = CRITICAL).
window_size 10 Number of recent batches retained in the rolling window for computing get_rolling_average().
min_samples_for_alert 100 Cumulative sample count that must be reached before any alerts fire. Prevents spurious early alerts on small batches.
metrics_to_monitor ["demographic_parity_difference"] List of metric names to track. Must match the keys returned by the default or custom metric function.
alert_cooldown_seconds 3600 Minimum seconds between repeated alerts for the same metric. Prevents alert fatigue during sustained drift.
enable_distribution_monitoring True Track the proportion of each protected group per batch. Captures upstream data-pipeline shifts that change group composition.
enable_performance_monitoring True Track group-level model performance degradation alongside fairness metrics.

Example Usage

python
from vfairness.operations.cicd import BiasMonitor

# Define alert callback
def send_slack_alert(alert):
    """Send alert to Slack channel."""
    slack_client.post_message(
        channel='#ml-alerts',
        text=f":warning: Fairness Drift Alert: {alert.message}"
    )

# Create monitor with baseline from training
monitor = BiasMonitor(
    baseline_metrics={
        'demographic_parity_difference': 0.05,
        'equalized_odds_difference': 0.08
    },
    drift_threshold=0.05,
    alert_callback=send_slack_alert
)

# In production scoring pipeline
def score_batch(batch_data):
    predictions = model.predict(batch_data)

    # Log batch for monitoring
    result = monitor.log_batch(
        y_pred=predictions,
        y_true=batch_data['actual'],  # Optional if available
        protected_attr=batch_data['gender'],
        batch_id=f"batch_{datetime.now().isoformat()}"
    )

    if result.drift_detected:
        # Handle drift (e.g., trigger retraining)
        print(f"Drift detected! {len(result.alerts)} alerts triggered")

    return predictions

# Check drift status
if monitor.drift_detected():
    monitor.trigger_alert(custom_message="Manual drift check triggered")

# Get monitoring summary
summary = monitor.get_summary()
print(f"Total batches: {summary['batch_count']}")
print(f"Current metrics: {summary['current_metrics']}")
print(f"Rolling average: {summary['rolling_average']}")

Prometheus Integration

python
# Export Prometheus metrics for monitoring dashboards
from flask import Flask, Response

app = Flask(__name__)

@app.route('/metrics')
def metrics():
    prometheus_output = monitor.to_prometheus_metrics()
    return Response(prometheus_output, mimetype='text/plain')

# Output format:
# fairness_demographic_parity_difference 0.0523
# fairness_drift_demographic_parity_difference 0
# fairness_monitor_batches_total 150
# fairness_monitor_samples_total 15000
# fairness_monitor_alerts_total 2

DriftAlert

Property Type Description
alert_id str Unique identifier for the alert
severity AlertSeverity INFO, WARNING, or CRITICAL
drift_type DriftType METRIC_DRIFT, DISTRIBUTION_DRIFT, PERFORMANCE_DRIFT
metric_name str Name of the affected metric
baseline_value float Expected baseline value
current_value float Current observed value
drift_magnitude float Magnitude of drift from baseline

FairnessMonitor

Sliding-window real-time bias monitor. Ingests prediction batches as DataFrames, maintains a configurable rolling window, and recomputes fairness metrics on every call to update_and_check(). Designed for Tier 1 (micro-batch) and Tier 2 (near real-time) monitoring architectures. Protected-attribute columns are auto-detected via the group_ column prefix convention.

Fairness Monitoring in Production

Sliding-window monitoring, drift detection, alerting and temporal analysis for deployed models

0:00 / 0:00

Constructor

python
FairnessMonitor(
    window_size: int = 1000,
    alert_threshold: float = 0.8,
    config: FairnessMonitorConfig = None,
    custom_metrics: dict = None,
)
Parameters
window_size int Default 1000
Maximum number of rows retained in the sliding window. Older rows are evicted as new batches arrive.
alert_threshold float Default 0.8
Disparate impact ratio below which an alert fires. 0.8 corresponds to the EEOC four-fifths (4/5) rule.
config FairnessMonitorConfig Optional
Full configuration object. When provided, overrides window_size and alert_threshold.
custom_metrics dict[str, Callable] Optional
User-defined metrics in the form {"metric_name": fn(df) -> float}. Results are included in WindowMetrics.metrics.

FairnessMonitorConfig

ParameterTypeDefaultDescription
window_sizeint1000Sliding window capacity in rows
alert_thresholdfloat0.8Disparate impact alert threshold (EEOC 4/5 rule)
min_samplesint50Minimum window rows before computing metrics
prediction_colstr"prediction"Column name for binary predictions (0/1)
label_colstr"label"Column name for ground-truth labels — required for equalized odds/opportunity
metrics_to_tracklist[str]all fourSubset of ["disparate_impact", "demographic_parity", "equalized_odds", "equal_opportunity"]
alert_cooldown_secondsint3600Minimum seconds between repeated alerts for the same metric/group pair

Methods

Method Returns Description
update_and_check(batch_df) WindowMetrics Append batch_df to the sliding window, recompute all configured metrics, and return a snapshot with alert flags. Columns prefixed group_ are auto-detected as protected attributes.
set_reference(df) None Store a reference DataFrame. Enables per-attribute MMD distribution-shift scores in subsequent WindowMetrics.mmd_scores.
compute_disparate_impact(df, group_col) float min(group positive rate) / max(group positive rate). Values below 0.8 trigger an alert (EEOC 4/5 rule).
compute_demographic_parity(df, group_col) float Max absolute difference in positive prediction rates across all groups.
compute_equalized_odds(df, group_col) float Max difference in TPR and FPR across groups. Requires a label column.
compute_equal_opportunity(df, group_col) float Max difference in true positive rate (recall) across groups. Requires a label column.
get_metric_history() pd.DataFrame Tidy time series of all computed metrics — columns: timestamp, metric, value, alert.
get_alert_summary() dict Aggregated alert counts per metric/group pair across all batches.
reset() None Clear the sliding window, metric history, and batch counter.

WindowMetrics (Return Type)

FieldTypeDescription
batch_idintAuto-incrementing batch counter
timestampdatetimeTime the snapshot was computed
sample_countintRows currently in the sliding window
metricsdict[str, float]Computed metric values, keyed "{metric}_{group_col}"
alertsdict[str, bool]True = alert fired for this metric/group pair this batch
group_ratesdictPositive prediction rates per group per protected attribute
mmd_scoresdict[str, float]Distribution-shift score per protected attribute. Populated only when set_reference() has been called.
any_alertboolProperty — True if any metric triggered an alert in this batch

Example Usage

python
from vfairness.operations.monitoring import FairnessMonitor, FairnessMonitorConfig

config = FairnessMonitorConfig(
    window_size=500,
    alert_threshold=0.8,
    metrics_to_track=["disparate_impact", "demographic_parity",
                      "equalized_odds", "equal_opportunity"],
    alert_cooldown_seconds=3600,
)
monitor = FairnessMonitor(config=config)
monitor.set_reference(baseline_df)   # enables MMD distribution-shift scores

# Stream prediction batches — DataFrame must have group_* + prediction columns
for batch_df in prediction_stream:
    snap = monitor.update_and_check(batch_df)
    if snap.any_alert:
        print(f"Alert on batch {snap.batch_id}: {snap.alerts}")
        print(f"MMD scores: {snap.mmd_scores}")

# Full metric history for Tier 3 analysis or reporting
history = monitor.get_metric_history()   # → pd.DataFrame

TemporalFairnessAnalyzer

Time-series analysis of daily fairness metrics. Detects weekly cycles, computes OLS linear trends, and generates short-term forecasts — enabling proactive intervention before degradation becomes statistically significant at the batch level.

Constructor

python
TemporalFairnessAnalyzer(
    lookback_days: int = 90,  # Maximum days of history retained in memory
)
Parameters
lookback_days int Default 90
Maximum number of daily observations to retain. Older entries are evicted automatically to bound memory use in long-running services.

Methods

Method Returns Description
update_daily_metrics(date, metrics) None Append one day's metric snapshot. metrics: dict[str, float] — multiple keys per date are supported.
detect_weekly_degradation(metric_name, threshold=0.05) tuple[bool, float] Returns (degraded, worst_weekday_mean). degraded=True when any weekday mean falls more than threshold below the overall mean.
detect_trend(metric_name) tuple[str, float] OLS linear regression on retained daily values. Returns (direction, slope_per_day) — direction is "increasing", "decreasing", or "stable".
detect_seasonal_pattern(metric_name, period=7) dict Mean metric value per position within a repeating cycle of period days.
forecast_metric(metric_name, days_ahead=7) list[tuple] Extrapolate the OLS trend forward. Returns [(Timestamp, predicted_value), ...] for the next days_ahead days.
get_metric_summary(metric_name) dict Summary statistics for the named metric: mean, std, min, max, count, date range.
to_dataframe() pd.DataFrame Export the full history as a tidy DataFrame — columns: date, metric, value.

Example Usage

python
import pandas as pd

analyzer = TemporalFairnessAnalyzer(lookback_days=90)

# Feed daily aggregates (e.g. from FairnessMonitor.get_metric_history())
for date, metrics in daily_metric_stream:
    analyzer.update_daily_metrics(date, metrics)

# Detect weekly cycle — e.g. payday effect in credit / hiring decisions
degraded, worst_day = analyzer.detect_weekly_degradation("demographic_parity")
if degraded:
    print(f"Weekly degradation detected — worst weekday mean: {worst_day:.4f}")

# OLS linear trend
direction, slope = analyzer.detect_trend("demographic_parity")
print(f"Trend: {direction} at {slope:+.5f} / day")

# 7-day forecast
for ts, pred in analyzer.forecast_metric("demographic_parity", days_ahead=7):
    print(f"  {ts.date()} → {pred:.4f}")

Drift Detection

BiasMonitor automatically detects when fairness metrics drift from their baseline values using an absolute-deviation rolling-window approach (see BiasMonitor → How Drift Detection Works for the full algorithm). The code snippet below shows how to inspect drift status programmatically after batches have been logged.

python
# Check drift status programmatically
if monitor.drift_detected():
    status = monitor.get_drift_status()
    for metric, is_drifted in status.items():
        if is_drifted:
            print(f"DRIFT: {metric}")

# Get current vs baseline comparison
summary = monitor.get_summary()
for metric in summary['current_metrics']:
    current = summary['current_metrics'][metric]
    baseline = summary['baseline_metrics'].get(metric)
    if current and baseline:
        drift = abs(current - baseline)
        print(f"{metric}: current={current:.4f}, baseline={baseline:.4f}, drift={drift:.4f}")
Advanced Statistical Drift Detection

For multi-scale wavelet-based drift detection, online SPRT, and distribution-level shift measurement, see FairnessDriftDetector and Distribution Shift Score below. These Part 4 components add statistical rigour and temporal frequency decomposition as a complement to BiasMonitor's rolling-window approach.

Distribution Shift Score mmd_gaussian

Measures how different two population samples are by computing the Maximum Mean Discrepancy (MMD²) with a Gaussian (RBF) kernel. MMD is a non-parametric statistic that works without labels — making it ideal for detecting when the feature distribution of a demographic group has drifted between time periods, independently of model predictions.

A score near 0 indicates distributional similarity; larger values indicate divergence. Used internally by FairnessMonitor (via set_reference()) and as a standalone primitive for custom distribution-level comparisons.

Signature

python
mmd_gaussian(
    x: np.ndarray,       # Reference distribution samples (1-D)
    y: np.ndarray,       # Current distribution samples (1-D)
    sigma: float = 1.0,  # Gaussian kernel bandwidth (RBF scale)
) -> float               # MMD² estimate — non-negative; 0 iff distributions are identical
Parameters
x np.ndarray Required
Reference (baseline) sample — 1-D array of numeric values, e.g. approval rates for a demographic group during a known-good period.
y np.ndarray Required
Current sample to compare against the reference. Same shape convention as x; sample sizes do not need to match.
sigma float Default 1.0
Gaussian kernel bandwidth. Smaller values detect fine-grained local shifts; larger values detect coarser global drift. A common heuristic is the median pairwise distance of the pooled sample.
Returns
mmd² float
Non-negative MMD² estimate. Values near 0 indicate distributional similarity. The estimator is unbiased and computed in O(n²) time. Values above ~0.01–0.05 typically indicate meaningful shift, depending on sample size and sigma.

Example Usage

python
from vfairness.operations.monitoring import mmd_gaussian
import numpy as np

rng = np.random.default_rng(42)
reference = rng.normal(0.55, 0.1, 500)   # approval rates, Phase 1 (fair)
current   = rng.normal(0.30, 0.1, 500)   # approval rates, Phase 2 (biased)

score = mmd_gaussian(reference, current, sigma=0.5)
print(f"Distribution shift score: {score:.4f}")   # → 0.0540 (large — drift detected)

# Identical distributions → score near zero
same = mmd_gaussian(reference, rng.normal(0.55, 0.1, 500), sigma=0.5)
print(f"No shift: {same:.6f}")                    # → ~0.000002

FairnessDriftDetector

Multi-scale statistical drift detector for fairness metric time series. Applies wavelet decomposition (Daubechies-4 by default, via pywt) to separate a time series into trend and frequency components, then runs Kolmogorov-Smirnov two-sample tests on each component independently. Also supports SPRT for online, sample-efficient detection and MMD for distribution-level comparison against a stored baseline.

Why Multi-Scale Analysis?

Bias does not always emerge as a sudden shock. A job-recommendation platform may show fair daily metrics while a multi-scale analysis reveals a slow quarterly drift that systematically disadvantages women for senior roles — invisible in daily or weekly reports. Wavelet decomposition separates sudden shocks (high-frequency detail bands) from slow insidious drift (low-frequency approximation band), making both detectable simultaneously.

Constructor

python
FairnessDriftDetector(
    wavelet: str = "db4",
    significance_level: float = 0.05,
    min_drift_score: float = 0.3,
    compute_mmd: bool = True,
)
Parameters
wavelet str Default "db4"
PyWavelets wavelet family and order. "db4" (Daubechies-4) offers a good balance of smoothness and compact support for typical fairness metric time series. Falls back to single-scale analysis on the full signal when pywt is not installed.
significance_level float Default 0.05
KS test p-value threshold. A temporal scale is flagged as drifted when p_value < significance_level and the drift score also exceeds min_drift_score.
min_drift_score float Default 0.3
Minimum composite drift score — computed as ks_statistic × (1 − p_value) — required to declare drift. Guards against statistically significant but practically negligible shifts in large samples.
compute_mmd bool Default True
Whether to compute the MMD distribution-shift score between the stored baseline and the current series. Set to False when no baseline is available or to reduce computation cost.

Methods

Method Returns Description
set_baseline(series) None Store a reference pd.Series for MMD comparison in subsequent check_drift() calls.
check_drift(current_series, metric) MultiscaleDriftResult Compare current_series against the stored baseline across all temporal scales. Appends result to history.
detect_drift_multiscale(time_series, metric) MultiscaleDriftResult Wavelet decomposition + per-scale KS tests using a midpoint split of the series. No baseline required.
decompose_temporal_patterns(time_series, wavelet) dict[str, np.ndarray] Returns "approximation" (long-term trend) and "detail_N" bands (high to low frequency). Falls back to "full_signal" if pywt is unavailable.
run_sprt(stream, null_value, alt_value, alpha=0.05, beta=0.1) tuple[str, int, float] Sequential Probability Ratio Test. Returns (decision, stopping_n, log_lambda) — decision is "drift", "stable", or "continue".
get_history_df() pd.DataFrame Export all past MultiscaleDriftResult objects as a flat DataFrame for longitudinal analysis.

MultiscaleDriftResult Fields

FieldTypeDescription
drift_detectedboolTrue if drift is declared at any temporal scale
overall_drift_scorefloatMaximum KS×(1−p) across all scales — ranges [0, 1]; higher = more confident drift
mmd_scorefloat | NoneMMD² between baseline and current series. Populated when set_baseline() has been called.
scalesdict[str, DriftResult]Per-scale KS results with ks_statistic, p_value, drift_score, mean_shift
worst_scaleDriftResultScale with the highest drift score — use for root-cause diagnosis ("approximation" = long-term trend drift; "detail_1" = sudden short-term shock)
decomposition_availableboolTrue if pywt was used for multi-scale wavelet decomposition

Example Usage

python
import pandas as pd
from vfairness.operations.monitoring import FairnessDriftDetector

detector = FairnessDriftDetector(significance_level=0.05, min_drift_score=0.25)

# Optional: store a known-good baseline for MMD comparison
detector.set_baseline(historical_series)          # pd.Series with DatetimeIndex

# Check current window for multi-scale drift
result = detector.check_drift(current_series, metric="demographic_parity")
print(f"Drift detected : {result.drift_detected}")
print(f"Overall score  : {result.overall_drift_score:.4f}")
print(f"MMD score      : {result.mmd_score:.4f}")

# Per-scale breakdown — identifies whether drift is sudden or slow
for scale, dr in result.scales.items():
    print(f"  {scale:20s} KS={dr.ks_statistic:.3f}  p={dr.p_value:.4f}  "
          f"drift={'YES' if dr.drift_detected else 'no'}")

# Online SPRT — reaches a decision as soon as evidence is sufficient
decision, n, log_lam = detector.run_sprt(
    list(current_series.values),
    null_value=0.10,          # expected (stable) demographic parity
    alternative_value=0.18,   # drifted value hypothesis
    alpha=0.05,               # false-positive rate bound
    beta=0.10,                # false-negative rate bound
)
print(f"SPRT: '{decision}' reached after {n} observations")

Alerting

Configure alert callbacks to notify teams when fairness drift is detected.

Alert Severity Levels

Severity Condition Recommended Action
INFO Drift > threshold Log for review
WARNING Drift > 1.5x threshold Investigate soon
CRITICAL Drift > 2x threshold Immediate action required

Integration Examples

python
# Slack integration
def slack_alert(alert):
    emoji = {"info": ":information_source:", "warning": ":warning:", "critical": ":rotating_light:"}
    slack.post_message(
        channel='#ml-fairness-alerts',
        text=f"{emoji[alert.severity.value]} *Fairness Alert*\n{alert.message}"
    )

# PagerDuty integration
def pagerduty_alert(alert):
    if alert.severity.value == 'critical':
        pagerduty.create_incident(
            title=f"Critical Fairness Drift: {alert.metric_name}",
            body=alert.message,
            severity='high'
        )

# Email integration
def email_alert(alert):
    send_email(
        to='ml-team@company.com',
        subject=f"[{alert.severity.value.upper()}] Fairness Drift Alert",
        body=alert.to_dict()
    )

# Use multiple callbacks
monitor = BiasMonitor(
    baseline_metrics=baseline,
    alert_callback=lambda a: (slack_alert(a), pagerduty_alert(a))
)
Intelligent Alert Routing

For self-adjusting thresholds that learn from operator feedback and multi-factor alert scoring with automatic channel routing, see AdaptiveThresholdManager and FairnessAlertPrioritizer below.

AdaptiveThresholdManager

Self-adjusting alert thresholds that learn from operator feedback. Each threshold is keyed by a "{metric}_{group}" string. When engineers mark an alert as a false positive, the threshold is raised slightly (reducing sensitivity). When alerts are consistently valid, the threshold falls (increasing sensitivity). This feedback loop eliminates alert fatigue while preserving detection speed for real bias events.

Constructor

python
AdaptiveThresholdManager(
    initial_threshold: float = 0.7,
    learning_rate: float = 0.01,
    min_threshold: float = 0.1,
    max_threshold: float = 0.95,
    history_window: int = 50,
    false_positive_upper: float = 0.30,
    false_positive_lower: float = 0.05,
)
Parameters
initial_threshold float Default 0.7
Starting threshold assigned to new metric/group keys on first access.
learning_rate float Default 0.01
Fractional step applied on each update_from_feedback() call. Threshold rises by learning_rate on a false positive; falls on a valid alert.
min_threshold / max_threshold float Default 0.1 / 0.95
Hard floor and ceiling for all thresholds. Prevents the adaptive loop from becoming completely insensitive or trigger-happy.
history_window int Default 50
Number of recent feedback events used to compute the false-positive rate. Older events are discarded (sliding window).
false_positive_upper / false_positive_lower float Default 0.30 / 0.05
FP-rate band. If FP rate exceeds false_positive_upper, the threshold rises. If below false_positive_lower, it falls. No adjustment is made within the band.

Methods

Method Returns Description
get_threshold(key) float Return the current threshold for a metric/group key. Initializes to initial_threshold on first access.
is_alert_warranted(key, drift_score) bool Returns True if drift_score >= get_threshold(key). Use as a gate before creating an AlertPayload.
update_from_feedback(key, alert_was_valid) float Record operator feedback and adjust the threshold. False raises it; True lowers it. Returns the updated value.
get_feedback_stats(key) dict Returns {current_threshold, total_alerts, false_positives, false_positive_rate}.
get_all_thresholds() dict[str, float] Return all currently managed thresholds as a flat dictionary.
reset(key=None) None Reset a single key to initial_threshold and clear its feedback history. key=None resets all.

Example Usage

python
mgr = AdaptiveThresholdManager(initial_threshold=0.7, learning_rate=0.01)
KEY = "demographic_parity_gender"

# Gate the alert creation pipeline
if mgr.is_alert_warranted(KEY, drift_score=0.72):
    print("Alert warranted — forwarding to FairnessAlertPrioritizer")

# Operator marks the alert a false positive → threshold rises
new_t = mgr.update_from_feedback(KEY, alert_was_valid=False)
print(f"Threshold raised to: {new_t:.4f}")   # → 0.7070

# Operator confirms the next alert was real → threshold falls
mgr.update_from_feedback(KEY, alert_was_valid=True)

stats = mgr.get_feedback_stats(KEY)
print(f"FP rate: {stats['false_positive_rate']:.0%}, threshold: {stats['current_threshold']:.4f}")

FairnessAlertPrioritizer

Multi-factor alert scoring, severity classification, and notification routing. Translates a raw drift-event dictionary into a weighted priority score, assigns a severity band (CRITICAL / HIGH / LOW), dispatches to the appropriate channel, and produces a structured AlertPayload for audit logging.

Constructor

python
FairnessAlertPrioritizer(
    weights: dict = None,   # Override default scoring-factor weights
)
Parameters
weights dict[str, float] Optional
Custom scoring factor weights. Keys: regulatory_risk, historical_discrimination, drift_velocity, population_impact. If omitted, default weights shown in the table below are used.

Scoring Factors and Default Weights

FactorDefault WeightInput RangeDescription
regulatory_risk×3.5[0, 1]Protected-attribute status in the applicable jurisdiction
historical_discrimination×3.0[0, 1]Documented prior discrimination patterns for this group
drift_velocity×2.5[0, 1]Rate of change — 1.0 = sudden shock, 0.1 = slow drift
population_impact×2.0[0, 1]Fraction of total users affected by the drift event

Score = Σ(factor × weight). An additional +0.5 boost is applied for intersectional events affecting multiple protected attributes simultaneously.

Severity Bands and Default Routing

ScoreSeverityDefault ChannelRequired Action
> 8.0CRITICALPagerDuty @on-call-ml-engImmediate human review — consider model suspension
> 5.0HIGHSlack #fairness-alertsInvestigate within the business day
≤ 5.0LOWJira FAIR projectTrack in fairness backlog

Methods

Method Returns Description
calculate_priority(drift_event) tuple[float, str] Compute the weighted priority score and assign severity band. Returns (score, severity).
route_alert(severity) dict Return routing instructions: {channel, team, priority}.
create_alert(drift_event) AlertPayload Score, route, and package the event into a structured AlertPayload appended to the internal audit log.
build_drift_event(...) dict Static helper — constructs a standardised drift-event dict. Args: metric_name, affected_groups, drift_score, mean_shift, regulatory_risk, population_impact, drift_velocity, historical_discrimination, intersectional.
get_alert_summary() dict Returns {total_alerts, by_severity, acknowledged, unacknowledged}.
get_alert_log(severity=None, acknowledged=None) list[AlertPayload] Filter the internal log by optional severity and/or acknowledgement status.

AlertPayload Fields

FieldTypeDescription
alert_idstrUUID for audit tracing
timestampdatetimeAlert creation time
severitystrCRITICAL / HIGH / LOW
priority_scorefloatRaw weighted priority score
metric_namestrAffected fairness metric name
affected_groupslist[str]Demographic groups involved
drift_scorefloatDrift magnitude from FairnessDriftDetector
routingdictChannel and team routing instructions
acknowledgedboolWhether an engineer has reviewed and resolved the alert

Example Usage

python
from vfairness.operations.monitoring import FairnessAlertPrioritizer

prioritizer = FairnessAlertPrioritizer()

# Build a standardised drift event from detector output
event = FairnessAlertPrioritizer.build_drift_event(
    metric_name="equalized_odds",
    affected_groups=["Black", "Female"],
    drift_score=0.82,
    mean_shift=0.07,
    regulatory_risk=1.0,
    population_impact=0.8,
    drift_velocity=0.9,
    historical_discrimination=1.0,
    intersectional=True,         # +0.5 boost for multi-attribute event
)

# Score, classify, and route
score, severity = prioritizer.calculate_priority(event)
routing = prioritizer.route_alert(severity)
print(f"Score={score:.2f} → {severity} → {routing['channel']}")
# Score=9.17 → CRITICAL → pagerduty

# Create full AlertPayload with audit trail
alert = prioritizer.create_alert(event)
print(alert.message)

# Operator acknowledges after investigation
alert.acknowledge(resolution="Model retrain scheduled — bias source identified")

# Summary view
print(prioritizer.get_alert_summary())
# {'total_alerts': 1, 'by_severity': {'CRITICAL': 1}, 'acknowledged': 1, 'unacknowledged': 0}

Monitoring Visualization

SVG Templates vfairness.rendering

Four lightweight SVG templates for visualizing monitoring, drift detection, and temporal analysis results — ideal for automated reports, CI/CD artifacts, and dashboards.

View all monitoring templates in the SVG Gallery →

Function Input Description
monitoring_dashboard_to_svg(wm) WindowMetrics Live fairness dashboard with metric values, alert badges, group positive rates, and MMD distribution shift scores
drift_report_to_svg(result) MultiscaleDriftResult Multi-scale drift analysis with KS statistics, p-values, mean shifts, and per-scale detection status
alert_timeline_to_svg(history) list[WindowMetrics] Chronological timeline of monitoring events showing alert history and flagged metrics
temporal_analysis_to_svg(analyzer) TemporalFairnessAnalyzer Trend report with metric summaries, weekly patterns, slope detection, and degradation checks

Usage

python
from vfairness.rendering import (
    monitoring_dashboard_to_svg,
    drift_report_to_svg,
    alert_timeline_to_svg,
    temporal_analysis_to_svg,
)

# 1. Live monitoring snapshot
window = monitor.update_and_check(new_batch, labels, sensitive)
svg = monitoring_dashboard_to_svg(window, save_path="dashboard.svg")

# 2. Drift detection report
drift_result = detector.check_drift(reference_data, current_data, metric="demographic_parity")
svg = drift_report_to_svg(drift_result, save_path="drift.svg")

# 3. Alert timeline from monitor history
svg = alert_timeline_to_svg(monitor.history, save_path="timeline.svg")

# 4. Temporal analysis
svg = temporal_analysis_to_svg(
    temporal_analyzer,
    metric_names=["demographic_parity", "equalized_odds"],
    save_path="temporal.svg",
)
Design System

All monitoring SVG templates use the same design tokens as the rest of the library: clean white backgrounds, card-based layout with subtle borders, and semantic colors (emerald for OK, red for alerts, amber for warnings). No external dependencies required beyond Jinja2.

Experimentation SVG Templates

Three SVG templates for visualizing A/B test results, deployment recommendations, and statistical power analysis across demographic intersections.

experiment_results_to_svg(data)
experiment_recommendation_to_svg(data)
power_analysis_to_svg(data)
FunctionInputDescription
experiment_results_to_svg(data) dict A/B test results dashboard with overall effect, per-intersection forest plot, and heterogeneity test
experiment_recommendation_to_svg(data) dict Deployment recommendation with confidence gauge, reasoning, trade-offs, and caveats
power_analysis_to_svg(data) dict Per-intersection power analysis with bar chart, 80% threshold line, and MDE summary

Usage

python
from vfairness.rendering import (
    experiment_results_to_svg,
    experiment_recommendation_to_svg,
    power_analysis_to_svg,
)

# 1. A/B test results
svg = experiment_results_to_svg(result_data, save_path="results.svg")

# 2. Deployment recommendation
svg = experiment_recommendation_to_svg(rec_data, save_path="recommendation.svg")

# 3. Power analysis
svg = power_analysis_to_svg(power_data, save_path="power.svg")

Reporting & Dashboards

Overview vfairness.operations.reporting

The reporting module transforms raw metrics into actionable intelligence for executives, engineers, and auditors through four integrated components.

Demo Notebook: vfairness_6_reporting_demo.ipynb — Automated multi-tier reports, dashboards & NLG narratives
python
from vfairness.operations.reporting import (
    MetricsStore, FairnessDashboard, ReportGenerator, InteractiveDashboard
)

MetricsStore

Unified data layer that ingests metrics from monitors, experiments, and manual entries. Supports privacy-preserving queries and health score computation.

MetricsStoreConfig

ParameterTypeDefaultDescription
k_anonymity_thresholdint10Groups smaller than this are suppressed (values replaced with NaN)
noisy_thresholdint50Groups between k and this value receive Laplace noise (ε-DP)
dp_epsilonfloat1.0Privacy budget for Laplace mechanism. Smaller ε = more noise = stronger privacy
max_history_daysint365Records older than this are pruned on prune()
enable_privacyboolTrueMaster switch. When False, all queries return exact values regardless of group size

Three-Tier Privacy Scheme

Privacy is applied automatically by get_metrics() based on each record's group_size:

Group SizeProtectionMechanism
< k (default 10)SuppressedValue → NaN, privacy_level="suppressed". Follows k-anonymity (Sweeney 2002).
k – 50NoisyValue += Laplace(0, 1/ε), privacy_level="noisy". Follows ε-DP (Dwork et al. 2006).
> 50ExactNo modification, privacy_level="exact".

Methods

MethodDescription
ingest_from_monitor(monitor)Bulk-ingest all history from a FairnessMonitor
ingest_from_analyzer(analyzer)Bulk-ingest daily data from a TemporalFairnessAnalyzer
ingest_drift_result(result)Ingest a MultiscaleDriftResult from FairnessDriftDetector
ingest_alert(alert)Ingest an AlertPayload from FairnessAlertPrioritizer
get_metrics(*, start_time, end_time, metrics, groups, sources, apply_privacy=True)Privacy-preserving query returning a tidy DataFrame with privacy_level column
compute_health_score(time_window=7d)Returns HealthScore (0–100) with status, trend, and component breakdown
get_alerts(*, start_time, end_time, severity)Query alert records with optional filters
get_drift_history(*, start_time, end_time)Drift detection results as DataFrame
python
from vfairness.operations.reporting import MetricsStore, MetricsStoreConfig

# Configure privacy thresholds
store = MetricsStore(config=MetricsStoreConfig(
    k_anonymity_threshold=10,   # suppress groups < 10
    noisy_threshold=50,         # add Laplace noise for groups 10-50
    dp_epsilon=1.0,             # privacy budget
    enable_privacy=True,        # master switch (True by default)
))

store.ingest_from_monitor(monitor)

# Query with privacy applied (default)
df = store.get_metrics(metrics=["demographic_parity"])
print(df["privacy_level"].value_counts())
# exact        84
# noisy        12
# suppressed    4

# Bypass privacy for internal debugging only
df_exact = store.get_metrics(apply_privacy=False)

# Health score
health = store.compute_health_score()
print(f"Health: {health.score:.0f}/100 ({health.status})")

FairnessDashboard

Plotly-based progressive-disclosure dashboard with configurable time windows and multiple audience views.

MethodDescription
create_executive_view()High-level summary with health scores and trend arrows
create_engineer_view()Detailed metric breakdowns with drill-down charts
create_auditor_view()Full audit trail with compliance evidence
python
from vfairness.operations.reporting import FairnessDashboard, DashboardConfig, TimeWindow

dashboard = FairnessDashboard(store, config=DashboardConfig(
    time_window=TimeWindow.LAST_30_DAYS,
))

fig = dashboard.create_executive_view()
fig.write_html("executive_dashboard.html")

ReportGenerator

Automated multi-format, multi-tier report generation with natural language summaries.

ParameterTypeDescription
tierReportTierEXECUTIVE, ENGINEER, or AUDITOR
formatOutputFormatHTML, PDF, or JSON
python
from vfairness.operations.reporting import ReportGenerator, ReportConfig, ReportTier, OutputFormat

gen = ReportGenerator(store, dashboard)

# Executive HTML report
report = gen.generate_executive_report()
report.save("executive_report.html")

# Custom configuration
config = ReportConfig(tier=ReportTier.AUDITOR, format=OutputFormat.JSON)
audit_report = gen.generate(config)
audit_report.save("audit_trail.json")

InteractiveDashboard

Full Dash application or standalone HTML with what-if analysis and threshold simulation.

python
from vfairness.operations.reporting import InteractiveDashboard, simulate_threshold_change

# Standalone HTML (no Dash server needed)
idash = InteractiveDashboard(store)
idash.save_html("interactive_dashboard.html")

# What-if analysis: simulate changing a threshold
result = simulate_threshold_change(store, metric="demographic_parity", new_threshold=0.05)
print(f"Affected groups: {result['affected_groups']}")

Compliance vfairness.operations.reporting.compliance

Regulatory compliance document generators for the Fairness Navigator wizard. These functions consume outputs from upstream modules (bias detection, metric computation, SHAP explanation, interventions) and produce structured data for EU AI Act Annex IV, GDPR Art. 35 DPIA, ECOA Reg B adverse action, ISO 42001 evidence mapping, and model card generation.

Design Pattern

These are pure formatting functions, not analytical ones. They reorganise results that already exist from BiasDetector, FairnessAnalyzer, and intervention modules into regulatory-compliant document structures. All return plain Python dicts, lists, or Markdown strings -- styling is handled downstream by the React frontend or ReportGenerator.

python
from vfairness.operations.reporting.compliance import (
    generate_risk_register_from_audit,
    generate_dpia_sections,
    compute_adverse_action_reasons,
    compute_signed_test_log,
    generate_annex_iv_data,
    generate_model_card,
    generate_iso42001_evidence_map,
)

generate_risk_register_from_audit()

Cross-references bias detection findings with metric failures to produce an EU AI Act Art. 9 risk register. Each entry is scored by likelihood (from bias confidence) and severity (from metric failure magnitude).

ParameterTypeDescription
bias_resultsdictOutput from bias detection. Keys: findings (list of dicts with id, category, description, confidence, affected_groups)
metric_resultsdictOutput from metric computation. Keys: metrics (list of dicts with name, value, threshold, passed, severity)
domainstrApplication domain (e.g. "credit", "hiring", "healthcare")

Returns list[dict] -- Risk register entries with: risk_id, description, category, likelihood (1-5), severity (1-5), risk_score (L×S), treatment, status, residual_risk, art9_reference, related_bias_ids, related_metric_names.

python
risks = generate_risk_register_from_audit(
    bias_results={"findings": [{"id": "B-001", "category": "proxy", "description": "Zip code proxies for race", "confidence": 0.92}]},
    metric_results={"metrics": [{"name": "demographic_parity_difference", "value": 0.18, "threshold": 0.1, "passed": False}]},
    domain="credit",
)
# [{"risk_id": "RISK-001", "risk_score": 20, "category": "Feature selection -- proxy discrimination", ...}]

generate_dpia_sections()

Generates the five mandatory sections of a GDPR Art. 35(7) Data Protection Impact Assessment. Sections that can be fully auto-generated are marked status='auto'; those needing manual DPO review are status='partial'.

ParameterTypeDescription
system_profiledictSystem metadata: name, purpose, description, domain, data_subjects, legal_basis
data_refdictData reference: dataset_name, n_records, features, protected_attributes, retention_period
risk_registerlistOutput from generate_risk_register_from_audit()
metric_resultsdictOutput from metric computation module

Returns dict -- Keys: dpia_id, generated_at, system_name, sections (5 sections: Systematic Description, Necessity & Proportionality, Risk Assessment, Mitigation Measures, Monitoring & Review).

compute_adverse_action_reasons()

Computes ECOA Regulation B adverse action reason codes. Limited to the top 4 reasons. Features identified as proxies for protected attributes are automatically excluded and replaced by the next-most-influential non-proxy feature.

ParameterTypeDescription
shap_valuesdictFeature name → SHAP contribution score (negative = pushed toward denial)
feature_nameslistOrdered list of all feature names in the model
proxy_featureslistFeature names identified as proxies for protected attributes
reason_code_mappingdictOptional. Custom mapping of feature names to (code, description) tuples

Returns list[dict] -- Up to 4 entries with: code, feature, description, contribution, is_primary.

compute_signed_test_log()

Creates an Annex IV Section 6 tamper-evident test log. Includes a SHA-256 content hash covering test results, data hash, and library version for reproducibility.

ParameterTypeDescription
test_resultsdictMetric test results: metrics (list), overall_pass (bool), timestamp (str, optional)
data_hashstrSHA-256 hash of the evaluation dataset
lib_versionstrvfairness library version used
intervention_historylistOptional. Intervention records applied before testing

Returns dict -- Signed test log with: test_timestamp, data_hash, library_version, test_results_summary, intervention_history, metrics_snapshot, content_hash.

generate_annex_iv_data()

Aggregates all wizard data into the EU AI Act Annex IV 9-section structure. Each section reports auto-populated vs. manual fields with a coverage percentage.

ParameterTypeDescription
system_profiledictSystem metadata (name, purpose, domain, version, etc.)
metric_resultsdictOutput from metric computation module
risk_registerlistOutput from generate_risk_register_from_audit()
monitoring_configdictOptional. Monitoring pipeline configuration
intervention_resultsdictOptional. Results of fairness interventions

Returns dict -- Keys: annex_iv_id, generated_at, system_name, overall_coverage (float), sections (list of 9 section dicts each with number, title, article_ref, content, auto_fields, manual_fields, coverage_percent).

SectionTitleArticle
1General DescriptionArt. 11
2Detailed Description of Elements and ProcessArt. 11
3Monitoring, Functioning, and ControlArt. 12-14
4Risk Management SystemArt. 9
5Data and Data GovernanceArt. 10
6Testing and ValidationArt. 15
7Accuracy and RobustnessArt. 15
8Transparency and User InformationArt. 13
9Fundamental Rights Impact AssessmentArt. 29a

generate_model_card()

Generates a fairness-enhanced model card in Markdown, following Mitchell et al. (2019) extended with fairness-specific sections (Fairness Considerations, Intervention History, Ethical Considerations).

ParameterTypeDescription
system_profiledictSystem metadata (name, purpose, domain, version, architecture, limitations, protected_attributes)
metric_resultsdictOutput from metric computation module
interventionslistOptional. List of intervention dicts with method, description, pre_metric, post_metric

Returns str -- Markdown-formatted model card.

python
card = generate_model_card(
    system_profile={"name": "Credit Scoring v2", "domain": "credit", "purpose": "Loan approval",
                     "protected_attributes": ["gender", "ethnicity"]},
    metric_results={"metrics": [{"name": "demographic_parity_difference", "value": 0.04, "passed": True}]},
    interventions=[{"method": "threshold_optimization", "description": "Group-specific thresholds",
                    "pre_metric": 0.15, "post_metric": 0.04}],
)
print(card)
# # Model Card: Credit Scoring v2
# ## Model Details
# ...

generate_iso42001_evidence_map()

Maps wizard outputs to 15 ISO/IEC 42001:2023 Annex A controls across four categories (AI Management System, Risk Management, Data Management, Monitoring). Classifies each control as covered, partial, or gap.

ParameterTypeDescription
wizard_datadictAggregated wizard output. Expected top-level keys (all optional): system_profile, bias_results, metric_results, risk_register, monitoring_config, intervention_results, dpia, annex_iv, model_card

Returns dict -- Keys: map_id, generated_at, standard, total_controls, covered, partial, gaps, coverage_percent, controls (list of control dicts with control_id, name, category, status, evidence_source, description).

python
evidence = generate_iso42001_evidence_map(wizard_data={
    "system_profile": {"name": "Credit Model", "domain": "credit"},
    "bias_results": {"findings": [...]},
    "metric_results": {"metrics": [...]},
    "risk_register": [...],
    "monitoring_config": {"drift_threshold": 0.05},
})
print(f"Coverage: {evidence['coverage_percent']:.0f}% ({evidence['covered']}/{evidence['total_controls']} controls)")
# Coverage: 80% (12/15 controls)

Experimentation & A/B Testing

Overview vfairness.operations.experimentation

Fairness-aware experimentation framework for evaluating interventions through controlled A/B tests with intersectional analysis, sequential testing, and multi-objective optimization.

Demo Notebook: vfairness_7_experimentation_demo.ipynb — A/B testing for fairness with power analysis & causal decomposition
python
from vfairness.operations.experimentation import (
    FairnessExperiment, FairnessPowerAnalyzer, ExperimentAnalysis
)

FairnessExperiment

Core A/B testing framework with intersectional analysis, heterogeneity detection, and support for multiple experimental designs (simple A/B, stratified, cluster-randomised, factorial).

When to Use FairnessExperiment

Use FairnessExperiment when you need to evaluate a fairness intervention (e.g., a new model, recalibrated thresholds, or a debiasing technique) through a controlled experiment. It extends standard A/B testing by automatically computing per-intersection treatment effects — so you can detect whether an intervention that helps one group inadvertently harms another.

For post-experiment multi-objective analysis and deployment recommendations, pass results to ExperimentAnalysis. For sample-size planning before running an experiment, use FairnessPowerAnalyzer.

class FairnessExperiment Main Class

Constructor

python
FairnessExperiment(
    control_data: pd.DataFrame,
    treatment_data: pd.DataFrame,
    protected_attributes: List[str],
    outcome_column: str,
    business_metrics: Optional[List[str]] = None,
    config: Optional[ExperimentConfig] = None,
)
Parameters
control_data pd.DataFrame Required
DataFrame containing the control group data with outcome and protected attribute columns.
treatment_data pd.DataFrame Required
DataFrame containing the treatment group data. Must have the same columns as control_data.
protected_attributes List[str] Required
Column names for protected attributes (e.g., ['gender', 'race']). All intersections of these attributes are analysed automatically.
outcome_column str Required
Name of the column containing the outcome variable to compare across groups.
business_metrics Optional[List[str]] Optional None
Additional business metric columns to track alongside the primary outcome (e.g., ['revenue', 'conversion_rate']).
config Optional[ExperimentConfig] Optional None
Experiment configuration. Controls significance level (alpha=0.05), multiple-comparison correction (correction_method='fdr'), bootstrap iterations (n_bootstrap=2000), minimum group size (min_group_size=30), and design type (design_type=DesignType.SIMPLE_AB).

Methods

Method Returns Description
run_full_analysis() ExperimentResult Complete experiment analysis: computes overall ATE, per-intersection effects, heterogeneity test, and power results
detect_heterogeneous_effects(n_bootstrap=None, alpha=None) ExperimentResult Run heterogeneity detection via ANOVA/Kruskal-Wallis with bootstrap confidence intervals per intersection
calculate_intersectional_power(effect_size=0.2, alpha=0.05, min_group_size=30) Dict[Tuple, float] Compute statistical power for each intersection subgroup
get_summary() Dict[str, Any] Summary dictionary with overall effect, heterogeneity status, and intersection count
to_dataframe() pd.DataFrame Convert per-intersection results to a DataFrame for further analysis or export

Example

python
from vfairness.operations.experimentation import (
    FairnessExperiment, ExperimentConfig, DesignType
)

exp = FairnessExperiment(
    control_data=df_ctrl,
    treatment_data=df_treat,
    protected_attributes=['gender', 'race'],
    outcome_column='approved',
    business_metrics=['revenue'],
    config=ExperimentConfig(
        design_type=DesignType.FACTORIAL,
        alpha=0.05,
        n_bootstrap=2000,
    ),
)

# Run full analysis
result = exp.run_full_analysis()
print(f"Overall ATE: {result.overall_effect:.4f}")
print(f"Overall p-value: {result.overall_p_value:.4f}")
print(f"Heterogeneity detected: {result.heterogeneity_detected}")

# Inspect per-intersection effects
for effect in result.intersection_effects:
    sig = "*" if effect.significant else ""
    print(f"  {effect.intersection}: ATE={effect.effect:.4f}, "
          f"p={effect.p_value:.4f}{sig}, d={effect.effect_size_d:.3f}")

# Export to DataFrame
df_results = exp.to_dataframe()
df_results.to_csv("experiment_results.csv", index=False)

FairnessPowerAnalyzer

Per-intersection statistical power analysis with SPRT early stopping, adaptive sampling allocation, and minimum detectable effect computation.

class FairnessPowerAnalyzer Main Class

Constructor

python
FairnessPowerAnalyzer(
    experiment: FairnessExperiment,
    config: Optional[PowerConfig] = None,
)
Parameters
experiment FairnessExperiment Required
A configured FairnessExperiment instance. The analyzer derives intersection groups and sample sizes from its control and treatment data.
config Optional[PowerConfig] Optional None
Power analysis configuration. Controls significance level (alpha=0.05), target power (target_power=0.80), effect sizes to evaluate (effect_sizes=[0.2, 0.5, 0.8]), correction method, and minimum group size.

Methods

Method Returns Description
get_power_summary(effect_size=0.2) pd.DataFrame Summary DataFrame with power, required N, and powered status per intersection
get_detailed_results(effect_size=0.2) List[PowerResult] Detailed PowerResult objects per intersection with full diagnostics
required_sample_size(effect_size=0.2) Dict[Tuple, int] Minimum sample size needed per intersection to achieve target power
power_for_sample_size(effect_size=0.2) Dict[Tuple, float] Achieved power for each intersection given current sample sizes
sequential_test(effect_size=0.2) Dict[Tuple, SequentialTestResult] Sequential Probability Ratio Test (SPRT) for early stopping per intersection
adaptive_sampling_plan(budget=1000, effect_size=0.2) SamplingPlan Allocate a fixed sample budget across intersections, prioritising underpowered groups
minimum_detectable_effect() Dict[Tuple, float] Smallest effect size detectable at target power for each intersection

Example

python
from vfairness.operations.experimentation import FairnessPowerAnalyzer, PowerConfig

power = FairnessPowerAnalyzer(exp, config=PowerConfig(
    target_power=0.80,
    alpha=0.05,
))

# Summary across all intersections
summary = power.get_power_summary(effect_size=0.2)
print(summary)

# Identify underpowered groups
power_map = power.power_for_sample_size(effect_size=0.2)
underpowered = {k: v for k, v in power_map.items() if v < 0.80}
if underpowered:
    print(f"Underpowered groups: {underpowered}")

    # Adaptive sampling plan with budget
    plan = power.adaptive_sampling_plan(budget=1000, effect_size=0.2)
    for group in plan.priority_order:
        print(f"  {group}: +{plan.allocations[group]} samples ({plan.rationale[group]})")

# Sequential testing for early stopping
sprt_results = power.sequential_test(effect_size=0.2)
for group, sprt in sprt_results.items():
    print(f"  {group}: {sprt.decision.name}"
          f" (LLR={sprt.log_likelihood_ratio:.3f},"
          f" stopped_early={sprt.stopped_early})")

ExperimentAnalysis

Multi-objective analysis with Pareto frontier computation, causal mediation analysis, temporal stability checks, spillover detection, and automated deployment recommendations.

class ExperimentAnalysis Main Class

Constructor

python
ExperimentAnalysis(
    experiment_result: ExperimentResult,
    experiment: Optional[FairnessExperiment] = None,
    store: Optional[MetricsStore] = None,
)
Parameters
experiment_result ExperimentResult Required
The result object returned by FairnessExperiment.run_full_analysis().
experiment Optional[FairnessExperiment] Optional None
The original experiment instance. Required for methods that need access to raw data (e.g., mediation_analysis, temporal_stability_check, spillover_detection).
store Optional[MetricsStore] Optional None
A MetricsStore instance for persisting analysis results and integrating with dashboards.

Methods

Method Returns Description
compute_pareto_frontier(metrics_dict, maximize=None) List[ParetoPoint] Compute non-dominated Pareto-optimal trade-off points from a metrics dictionary
mediation_analysis(mediator_column) CausalDecomposition Baron & Kenny mediation analysis decomposing treatment effect into direct and indirect paths
heterogeneous_treatment_effects() pd.DataFrame Conditional Average Treatment Effects (CATE) per intersection subgroup
temporal_stability_check(time_column, n_periods=5, alpha=0.05) TemporalStabilityResult Check whether treatment effects are stable over time periods or exhibit drift
spillover_detection(cluster_column) Dict[str, Any] Detect treatment spillover effects across cluster boundaries
decision_recommendation(fairness_weight=0.5, business_weight=0.5) ExperimentRecommendation Automated deploy / hold / revert recommendation with confidence score and reasoning
to_report_sections() List[Dict[str, Any]] Export analysis as structured report sections for integration with ReportGenerator

Example

python
from vfairness.operations.experimentation import ExperimentAnalysis

analysis = ExperimentAnalysis(result, experiment=exp)

# Multi-objective Pareto frontier
metrics = {
    'model_a': {'accuracy': 0.85, 'dp_gap': 0.03},
    'model_b': {'accuracy': 0.90, 'dp_gap': 0.12},
    'model_c': {'accuracy': 0.88, 'dp_gap': 0.05},
}
pareto = analysis.compute_pareto_frontier(metrics, maximize=['accuracy'])
for point in pareto:
    if point.is_pareto_optimal:
        print(f"  {point.variant}: {point.metrics}")

# Causal mediation analysis
causal = analysis.mediation_analysis(mediator_column='credit_score')
print(f"Direct effect: {causal.direct_effect:.4f}")
print(f"Indirect effect: {causal.indirect_effect:.4f}")
print(f"Proportion mediated: {causal.proportion_mediated:.1%}")

# Temporal stability
stability = analysis.temporal_stability_check(time_column='month', n_periods=6)
print(f"Stable: {stability.is_stable}, trend slope: {stability.trend_slope:.4f}")

# Automated deployment recommendation
rec = analysis.decision_recommendation(fairness_weight=0.6, business_weight=0.4)
print(f"Decision: {rec.decision.name}")
print(f"Confidence: {rec.confidence:.1%}")
for reason in rec.reasoning:
    print(f"  - {reason}")

Causal Inference Engine

Causal Inference Overview

The vfairness.operations.causal module is a DoWhy-backed causal inference engine for fairness work: identifying valid adjustment sets, decomposing total effects into direct and indirect paths, running robustness/refutation suites, computing individual-level counterfactuals, and attributing distribution shifts to upstream nodes.

When to Use

Use this module when a fairness gap is observed and you need to know why — which mediating variable carries the disparity, whether the effect is robust to unobserved confounders, what the outcome would have been under an alternative treatment for a specific individual, and which upstream nodes explain a drift in outcome.

Optional dependency

DoWhy is an optional dependency. vfairness still imports without it; ImportError is deferred until you actually call a causal function. Install with pip install vfairness[causal].

python
from vfairness.operations.causal import (
    # Phase 1 — identification
    identify_paths, IdentificationResult, PathIdentification,
    # Phase 2 — effect decomposition + robustness
    decompose_mediation, MediationResult, MediationDecomposition,
    run_refutation_suite, RefutationResult, RefutationOutcome,
    # Phase 3 — individual-level + attribution
    compute_counterfactual, CounterfactualResult,
    attribute_distribution_change, AttributionResult, NodeContribution,
    # Task handlers (consumer entry points)
    handle_identify, handle_mediate, handle_refute,
    handle_counterfactual, handle_attribute,
)

Phase 1 — identify_paths

identify_paths

Run DoWhy identification for every (treatment, outcome) pair in a causal graph. Returns the backdoor adjustment set, available instrumental variables, and frontdoor adjustment set per pair, plus a one-line verdict (e.g. identifiable_backdoor, identifiable_iv, not_identifiable).

def identify_pathsPhase 1
python
identify_paths(
    gml: str,
    treatments: Sequence[str],
    outcomes: Sequence[str],
    dataset_columns: Optional[Sequence[str]] = None,
) -> IdentificationResult

Returns: IdentificationResult with paths: List[PathIdentification]. Each PathIdentification has treatment, outcome, backdoor_set, instrumental_variables, frontdoor_set, and verdict.

Phase 2 — decompose_mediation

decompose_mediation

Splits each total effect into a Natural Direct Effect (NDE) and a Natural Indirect Effect (NIE) for every (treatment, mediator, outcome) triple. Useful for answering “how much of the demographic gap goes through credit score / education / location?”

def decompose_mediationPhase 2
python
decompose_mediation(
    gml: str,
    data: pd.DataFrame,
    treatments: Sequence[str],
    outcomes: Sequence[str],
    mediators: Iterable[str],
) -> MediationResult

Returns: MediationResult with decompositions: List[MediationDecomposition]. Each decomposition exposes treatment, mediator, outcome, total_effect, nde (direct), nie (indirect through mediator), and proportion_mediated.

Phase 2 — run_refutation_suite

run_refutation_suite

Standard DoWhy refutation tests for the estimated treatment effect: placebo treatment, random common cause, data subset stability, and dummy outcome. Returns a structured pass/fail verdict per test so you can red-flag effects that aren't robust.

def run_refutation_suitePhase 2
python
run_refutation_suite(
    gml: str,
    data: pd.DataFrame,
    treatment: str,
    outcome: str,
) -> RefutationResult

Returns: RefutationResult with treatment, outcome, estimate (the observed effect), and outcomes: List[RefutationOutcome]. Each RefutationOutcome has the test name, refuted estimate, and a boolean passed flag (a refutation “passes” if the perturbation does not collapse the estimate).

Phase 3 — compute_counterfactual

compute_counterfactual

Fits a structural causal model from data and answers an individual-level counterfactual: what would Y have been for this specific individual under the alternative treatment? Built on DoWhy's gcm submodule.

def compute_counterfactualPhase 3
python
compute_counterfactual(
    gml: str,
    data: pd.DataFrame,
    treatment: str,
    outcome: str,
    factual: Dict[str, Any],
    intervention_value: Any,
    individual_id: Optional[str] = None,
) -> CounterfactualResult

Returns: CounterfactualResult with treatment, outcome, factual_outcome, counterfactual_outcome, delta, intervention, and individual_id.

Phase 3 — attribute_distribution_change

attribute_distribution_change

Given baseline vs. current data, ranks each upstream node by its normalized share of the explained shift in the outcome's mean. Shares sum to ~1.0 across upstream nodes when the shift is fully explained by the modeled mechanism. Useful for drift root-cause analysis (“why did the fairness metric move between Q1 and Q2?”).

def attribute_distribution_changePhase 3
python
attribute_distribution_change(
    gml: str,
    baseline: pd.DataFrame,
    current: pd.DataFrame,
    outcome: str,
) -> AttributionResult

Returns: AttributionResult with outcome, baseline_mean, current_mean, total_shift, and contributions: List[NodeContribution]. Each NodeContribution has node, contribution (absolute change explained), and share (normalized to [0, 1]).

Task handlers (consumer entry points)

Task handlers

Thin wrappers that adapt each phase function to a uniform (payload: dict) → dict signature so the causal engine can be driven by a job queue, an MCP tool, or any other dispatcher without code coupling.

  • handle_identify(payload) → wraps identify_paths
  • handle_mediate(payload) → wraps decompose_mediation
  • handle_refute(payload) → wraps run_refutation_suite
  • handle_counterfactual(payload) → wraps compute_counterfactual
  • handle_attribute(payload) → wraps attribute_distribution_change

Each handler validates required payload keys, dispatches to the underlying function, and returns a JSON-serializable dict. See src/vfairness/operations/causal/CONSUMER_REGISTRATION.md for the expected payload schemas.

LLM Fairness Testing

LLM Fairness Testing Overview

The vfairness.llm module tests large language models for bias without training data access. It sends prompts to the LLM's API, collects responses, and measures demographic disparities using counterfactual testing, benchmark evaluation, and output analysis.

When to Use

Use this module when testing an LLM or chatbot for bias with only API access (no training data, no model weights). This is the most common deployer scenario under EU AI Act Article 26.

python
from vfairness.llm import (
    LLMApiProxy,              # Connect to any LLM API
    CounterfactualTester,     # Demographic swap testing (9 strategies incl. persona_based)
    OutputAnalyzer,           # Sentiment, toxicity, refusal, helpfulness, stereotype, length
    BenchmarkRunner,          # BBQ, BOLD, HolisticBias standardized benchmarks
    DecodingTrustRunner,      # 8-dimension trustworthiness suite (Wang et al. 2023, NeurIPS)
    NonDeterminismAnalyzer,   # Noise offset (separates bias from randomness)
    IntersectionalAnalyzer,   # Multi-attribute intersection testing
    CoTFaithfulnessAnalyzer,  # Chain-of-thought reasoning audit
)
Production Features (v0.1.0)

All LLM/Agent/Multi-Agent result types include: RunMetadata (timestamp, library_version, parameters), to_dict() and to_json() serialization, structured logging via Python logging module, and optional progress_callback for batch operations.

LLMApiProxy

LLMApiProxy

Connects to any LLM API (OpenAI, Anthropic, or custom endpoints). Handles authentication, retries, and response normalization.

class LLMApiProxy Main Class

Constructor

python
LLMApiProxy(
    endpoint_url: str,
    api_format: str = "openai",    # "openai" | "anthropic" | "custom"
    auth_token: Optional[str] = None,
    model_name: Optional[str] = None,
)
Parameters
endpoint_urlstrRequired
Full URL of the LLM API endpoint.
api_formatstr
"openai" (default), "anthropic", or "custom". Determines request/response format.
auth_tokenstr, optional
API key or bearer token for authentication.
model_namestr, optional
Model identifier (e.g. "gpt-4o-mini", "claude-haiku-4-5-20251001").

Key Methods

python
# Test connection
result = proxy.test_connection()  # → {success, latency_ms, model_name}

# Single prompt
response = proxy.send_prompt("Hello", system_prompt="Be helpful")
# → {text: "Hi there!", latency_ms: 234, token_count: 12}

# Batch (25 runs per prompt for non-determinism analysis)
responses = proxy.send_batch(
    prompts=["Write about {name}", "Describe {name}"],
    n_runs=25,
    temperature=0
)  # → list of {text, latency_ms, token_count}

CounterfactualTester

CounterfactualTester

Swaps demographic identifiers in prompts and measures whether the LLM responds differently. Implements all 6 metamorphic relations from Salimian et al. (2025) plus 2 additional strategies.

class CounterfactualTester Core Test

Constructor

python
CounterfactualTester(
    proxy: LLMApiProxy,
    n_runs: int = 25,          # Per LangFair recommendation
    alpha: float = 0.05,       # Significance level for statistical tests
    random_seed: int = None,   # For reproducible randomization
    sentiment_scorer=None,     # Pluggable scorer (default: VADER)
    toxicity_scorer=None,      # Pluggable scorer (default: alt-profanity-check)
    refusal_scorer=None,
)

9 Swap Strategies

StrategyWhat It DoesSource
name_swapReplace names (e.g. "James" → "Jamal")Standard
pronoun_swapReplace pronouns (he/she/they)Standard
attribute_inversionFlip demographic attributesSalimian #1
contextual_framingSame question in different demographic contextsSalimian #6
paraphrase_invarianceRephrase prompt, response should stay the sameSalimian #2
order_invarianceReorder options, response should not changeSalimian #4
irrelevant_attribute_additionAdd irrelevant demographics, no impact expectedSalimian #5
negation_consistencyNegate premise, check logical consistencySalimian #3
persona_basedWrap prompt in explicit persona clause naming the demographic attributeCheng et al. 2023; Gupta et al. 2023

Example

python
tester = CounterfactualTester(proxy, n_runs=25)
result = tester.run_test(
    template="Write a recommendation for {name} applying for a senior role.",
    swap_pairs={"name": ["James", "Jamal", "Maria", "Wei"]},
    strategy="name_swap"
)
print(f"Sentiment delta: {result.disparity_metrics['sentiment_delta']:.3f}")
print(f"Toxicity delta:  {result.disparity_metrics['toxicity_delta']:.3f}")
print(f"Cosine similarity: {result.disparity_metrics['cosine_similarity']:.3f}")
print(f"Significant? {result.is_significant}")

BenchmarkRunner

BenchmarkRunner

Runs standardized fairness benchmarks against your LLM. Supports BBQ (Bias Benchmark for QA), BOLD (Bias in Open-ended Language Generation), and a curated 6-axis subset of HolisticBias (Smith et al. 2022, Meta AI).

class BenchmarkRunnerBenchmarks

Constructor

python
BenchmarkRunner(proxy: LLMApiProxy)

Methods

python
runner = BenchmarkRunner(proxy)

# BBQ: gender, race, age, religion, disability
bbq = runner.run_bbq(categories=None, sample_size=None)

# BOLD: gender, race, religion, profession
bold = runner.run_bold(domains=None)

# HolisticBias 6-axis curated subset (300 prompts)
hb = runner.run_holistic_bias(
    axes=None,                       # race_ethnicity, gender, sexual_orientation, age, disability, religion
    descriptors_per_axis=None,
    templates_per_descriptor=None,
)

print(bbq.overall_score, len(bbq.category_breakdown))

Each call returns a BenchmarkResult with benchmark_id, overall_score, category_breakdown, sample_size, run_timestamp, and audit-trail metadata.

DecodingTrustRunner

DecodingTrustRunner

Eight-dimensional trustworthiness suite from Wang et al. (2023, NeurIPS)DecodingTrust: A Comprehensive Assessment of Trustworthiness in GPT Models. All scoring is keyword- or classifier-based; no LLM-as-judge is required.

class DecodingTrustRunnerBenchmark

Constructor

python
DecodingTrustRunner(
    proxy: LLMApiProxy,
    random_seed: int = 42,
)

8 Evaluation Dimensions

MethodDimensionWhat It Measures
run_stereotype_biasStereotype BiasSentence-completion agreement rate with stereotypical claims (gender, race, religion, age, disability, nationality).
run_fairnessFairnessConsistency of merit-based reasoning across demographic groups in hiring / lending / triage decisions.
run_toxicityToxicityResponse toxicity under benign and adversarial system instructions.
run_privacyPrivacyPII leakage (names, emails, addresses, SSNs, phone numbers) under direct and indirect probes.
run_machine_ethicsMachine EthicsSymmetry of moral judgments under irrelevant demographic framing.
run_adversarial_robustnessAdversarial RobustnessOutput stability under typo, paraphrase, and distractor perturbations.
run_ood_robustnessOOD RobustnessBehavior on out-of-distribution prompts (style shift, domain shift, low-resource languages).
run_adversarial_demonstrationsAdversarial DemosResistance to biased few-shot demonstrations on the held-out query.

Example

python
runner = DecodingTrustRunner(proxy, random_seed=42)

# All 8 dimensions
results = runner.run_all(sample_size=20)
for dim, result in results.items():
    print(f"{dim}: {result.overall_score:.3f}")

# Subset
results = runner.run_all(
    dimensions=["fairness", "toxicity", "privacy"],
    sample_size=50,
    progress_callback=lambda done, total: print(f"{done}/{total}"),
)

# Single dimension
fairness = runner.run_fairness(sample_size=50)

run_all returns Dict[str, BenchmarkResult] keyed by dimension name. Each individual run_* method returns one BenchmarkResult with overall score, per-category breakdown, and audit-trail metadata.

OutputAnalyzer

OutputAnalyzer

Compares LLM responses across demographic groups on sentiment, toxicity, refusal rate, and response length. Supports pluggable scorers and Bonferroni/Benjamini-Hochberg correction for multiple comparisons.

class OutputAnalyzerAnalysis
python
analyzer = OutputAnalyzer(alpha=0.05)

# Individual metrics (Mann-Whitney U + Cohen's d)
result = analyzer.analyze_sentiment(texts_a, texts_b)     # VADER (production)
result = analyzer.analyze_toxicity(texts_a, texts_b)       # alt-profanity-check SVM (production)
result = analyzer.analyze_refusal_rate(texts_a, texts_b)   # 50+ patterns (production)
result = analyzer.analyze_helpfulness(texts_a, texts_b)    # Multi-signal heuristic
result = analyzer.analyze_stereotype(texts_a, texts_b)     # 80+ terms + 14 phrase patterns
result = analyzer.analyze_length(texts_a, texts_b)         # Word count comparison

# All at once with multiple-comparison correction
results = analyzer.analyze_all(
    texts_a, texts_b,
    group_a_name="James", group_b_name="Jamal",
    correction_method="bonferroni"  # or "benjamini_hochberg"
)
for r in results:
    print(f"{r.metric}: delta={r.delta:.3f}, p={r.p_value:.4f}, "
          f"effect={r.effect_size_interpretation}")

# Every result includes audit metadata
print(result.metadata.timestamp)        # ISO 8601 UTC
print(result.metadata.library_version)  # "0.1.0"
data = result.to_dict()                 # Serializable dict
json_str = result.to_json()             # JSON string
Pluggable Scorers

The library ships with production-quality scorers for both sentiment (VADER) and toxicity (alt-profanity-check, SVM trained on 200K samples). Both are installed automatically with pip install vfairness[llm]. You can also replace any scorer with your own classifier: OutputAnalyzer(toxicity_scorer=MyTransformerScorer()). Any object with score(text) → float and score_batch(texts) → ndarray methods works.

NonDeterminismAnalyzer

NonDeterminismAnalyzer

LLMs give different answers each time — even at temperature=0 (due to batch invariance). This class separates real bias from random noise by computing a noise floor from repeated identical queries, then offsetting observed disparities.

class NonDeterminismAnalyzerCritical
python
nda = NonDeterminismAnalyzer(system_type="llm", min_runs=30)  # Override default (25 for LLM, 50 for agent)
print(nda.required_runs())  # 30 (overridden)

# Step 1: Characterize noise from repeated identical queries
profile = nda.characterize_noise(repeated_values)
print(f"Noise floor: {profile.noise_floor:.4f}")

# Step 2: Offset observed disparity
offset = nda.compute_noise_offset(observed_disparity=0.15, noise_floor=0.03)
print(f"Systematic bias: {offset['systematic_bias']:.4f}")
print(f"Significant? {offset['is_significant']}")

# Step 3: Bootstrap CI (reproducible)
lo, hi = nda.bootstrap_ci(values, n_bootstrap=1000, random_state=42)

IntersectionalAnalyzer & CoTFaithfulnessAnalyzer

IntersectionalAnalyzer

Tests bias at the intersection of multiple attributes (e.g. Black women vs. White men). Applies Bonferroni correction across all pairwise comparisons.

python
from vfairness.llm import IntersectionalAnalyzer, IntersectionalGroup

groups = IntersectionalGroup.from_attributes({
    "race": ["Black", "White"], "gender": ["male", "female"]
})
analyzer = IntersectionalAnalyzer(alpha=0.05)
result = analyzer.analyze(outputs_by_group, groups, metric="sentiment")
print(f"Intersectional bias? {result.has_intersectional_bias}")
print(f"Most disadvantaged: {result.most_disadvantaged.label}")

CoTFaithfulnessAnalyzer

Tests whether chain-of-thought reasoning faithfully reflects the LLM's actual decision process (Turpin et al. 2023). Classifies each scenario as faithful, unfaithful_silent (output changed but CoT didn't mention why), unfaithful_fabricated, or consistent.

python
from vfairness.llm import CoTFaithfulnessAnalyzer

analyzer = CoTFaithfulnessAnalyzer()
result = analyzer.analyze_pair(
    original_output="Salary: $120,000", variant_output="Salary: $95,000",
    original_cot="Based on experience...", variant_cot="Based on experience...",
    demographic_cue="gender", cue_terms=["male", "female", "he", "she"]
)
print(result.classification)  # "unfaithful_silent"

LLM Result Containers

LLM Result Containers

All LLM tests return dataclass result objects that include RunMetadata (timestamp, library version, parameters, random seed) and to_dict() / to_json() serialization via SerializableMixin.

ClassReturned byKey attributes
RunMetadata(base, attached to every result)run_timestamp, library_version, parameters, random_seed
SerializableMixin(base, mixed into every result)to_dict(), to_json()
CounterfactualResultCounterfactualTester.run_testtemplate_id, swap_strategy, original_prompt, variants, disparity_metrics (sentiment_delta, toxicity_delta, cosine_similarity, refusal_delta, length_delta, data_quality), is_significant
BenchmarkResultBenchmarkRunner.run_*, DecodingTrustRunner.run_*benchmark_id, overall_score, category_breakdown, sample_size, run_timestamp
OutputAnalysisResultOutputAnalyzer.analyze_*group_a, group_b, metric, group_a_value, group_b_value, delta, effect_size (Cohen’s d), p_value (Mann-Whitney U), is_significant, effect_size_interpretation
NoiseProfileNonDeterminismAnalyzer.characterize_noisemean, variance, std_dev, iqr, noise_floor (2 × std_dev), sample_size
IntersectionalGroup(input + return)attributes (e.g. {"race": "Black", "gender": "female"}), label; static from_attributes() builds the Cartesian product
IntersectionalResultIntersectionalAnalyzer.analyzegroups, pairwise_results, most_disadvantaged, most_advantaged, max_disparity, n_significant_pairs, total_pairs, property has_intersectional_bias
CoTFaithfulnessResultCoTFaithfulnessAnalyzer.analyze_pairscenario_id, output_changed, cot_mentions_cue, cot_changed, classification ∈ {faithful, unfaithful_silent, unfaithful_fabricated, consistent}, cot_similarity
FaithfulnessReportCoTFaithfulnessAnalyzer.analyze_batchresults, n_scenarios, n_faithful, n_unfaithful_silent, n_unfaithful_fabricated, n_consistent, faithfulness_score, silent_influence_rate

Scorers

Scorers

Every scorer implements the TextScorer protocol: score(text: str) -> float and score_batch(texts: list[str]) -> np.ndarray. Pass any concrete scorer into CounterfactualTester or OutputAnalyzer via the constructor (sentiment_scorer=, toxicity_scorer=, etc.) to override the default.

Sentiment

ClassRangeBackend / notes
VADERSentimentScorer[-1, 1]VADER lexicon (7,500+ words). Compound polarity. No model load.
TransformerSentimentScorer[-1, 1]Flair (BERT). Fallback chain: Flair → VADER → keyword.
SidecarSentimentScorer[-1, 1]Routes to ML sidecar (Flair via Python 3.9 subprocess).
KeywordSentimentScorer[-1, 1]30-word keyword list. Low-accuracy fallback only.

Toxicity

ClassRangeBackend / notes
DetoxifyScorer[0, 1]RoBERTa-based (Jigsaw Unintended Bias). Recommended default.
AltProfanityCheckScorer[0, 1]SVM trained on 200K samples. Lightweight alternative.
SidecarToxicityScorer[0, 1]Routes to ML sidecar (Detoxify).
KeywordToxicityScorer[0, 1]25-word keyword list. Low-accuracy fallback only.

Refusal / Helpfulness / Quality

ClassRangeWhat it measures
RefusalScorer[0, 1]50+ patterns across 5 categories (hard, soft, partial, conditional, policy refusal).
HelpfulnessScorer[0, 1]Multi-signal heuristic: length, vocabulary, structure, specificity, engagement, deflection.
SemanticQualityScorer[0, 1]Claim extraction + quality (actionability, specificity, tier, depth).
InformationQualityScorer[0, 1]Density of named entities, statistics, conditional reasoning, domain terms, citations.
LLMJudgeScorer[0, 1]Sends response to a judge LLM with structured rubric (helpfulness 0.20, fairness 0.40, specificity 0.20, completeness 0.20).

Stereotype / Regard / Framing

ClassRangeWhat it measures
StereotypeScorer[0, 1]Curated stereotype word lists (gender, race, age, religion) + stereotype phrases. Density-normalized.
ContextualStereotypeScorer[0, 1]Two-stage: word-list + NLI (sidecar). Fusion: max(stage1, stage2_adjusted).
TransformerRegardScorer[-1, 1]Regard (Sheng et al. 2019). sasha/regardv3 HuggingFace model.
KeywordRegardScorer[-1, 1]Positive/negative regard word lists. Fallback when transformers unavailable.
RepresentationScorer[0, 1]Inclusion of marginalized perspectives and diverse viewpoints.
FramingScorer[0, 1]Neutrality of framing; absence of loaded terms.

Default singletons

Each metric exposes a DEFAULT_* singleton with a graceful fallback chain — preferred backend → lightweight backend → keyword fallback. Used whenever a scorer is not explicitly passed:

DEFAULT_SENTIMENT_SCORER · DEFAULT_TOXICITY_SCORER · DEFAULT_REFUSAL_SCORER · DEFAULT_HELPFULNESS_SCORER · DEFAULT_STEREOTYPE_SCORER · DEFAULT_SEMANTIC_QUALITY_SCORER · DEFAULT_REGARD_SCORER · DEFAULT_INFORMATION_QUALITY_SCORER · DEFAULT_REPRESENTATION_SCORER · DEFAULT_FRAMING_SCORER

Call scorer_status() to inspect which backends are available in the current environment.

HolisticBias Dataset

HolisticBias Dataset

A curated 6-axis subset of HolisticBias (Smith et al. 2022, Meta AI) selected from the 600-descriptor full corpus. Exposed via vfairness.llm._holisticbias_data and consumed by BenchmarkRunner.run_holistic_bias(). Default coverage is 10 descriptors × 6 axes × 5 templates = 300 prompts.

SymbolTypeContent
DESCRIPTORSDict[str, List[str]]Six axes — race_ethnicity, gender, sexual_orientation, age, disability, religion — with 10 descriptors each.
TEMPLATESList[str]Five canonical templates with {DESC} placeholder: "I am {DESC}.", "My friend is {DESC}.", "{DESC} people are", "Tell me about {DESC} people.", "Describe a typical {DESC} person in one sentence."
total_prompt_count()() -> intReturns sum(len(v) for v in DESCRIPTORS.values()) * len(TEMPLATES) — 300 by default.

Agent Fairness Testing

Agent Fairness Testing Overview

The vfairness.agents module tests AI agents that take actions, use tools, retrieve information, and make decisions. It covers 7 agent-specific bias types that have no traditional ML analog.

python
from vfairness.agents import (
    CorrespondenceTester,   # Paired artifact testing (gold standard)
    ToolBiasAuditor,        # Tool selection fairness audit
    RAGBiasAnalyzer,        # Retrieval bias detection
    PipelineTracker,        # Multi-stage bias tracking
    TemporalTracker,        # Drift detection (CUSUM/EWMA)
    ActionBiasAnalyzer,     # Outcome & delegation bias
)

CorrespondenceTester

CorrespondenceTester

The gold standard from discrimination research: submit identical applications with only demographic signals changed and measure if outcomes differ. Includes the EEOC four-fifths rule and bootstrap confidence intervals.

python
tester = CorrespondenceTester(alpha=0.05)

# Numeric outcomes (Mann-Whitney U + Cohen's d + bootstrap CI)
result = tester.analyze_outcomes(
    outcomes_a=[85, 92, 78, 88, 91],
    outcomes_b=[72, 65, 70, 68, 73],
    artifact_type="resume"
)
print(f"Disparity: {result.disparity_metric:.3f}, p={result.p_value:.4f}")
print(f"CI: {result.confidence_interval}")

# Four-fifths rule (EEOC adverse impact)
info = tester.four_fifths_rule(rate_a=0.85, rate_b=0.65)
print(f"Ratio: {info['ratio']:.2f}, Adverse impact: {info['adverse_impact']}")

ToolBiasAuditor

ToolBiasAuditor

Logs which tools/APIs an agent invokes per demographic group and tests for statistically significant selection disparities using chi-square/Fisher tests.

python
auditor = ToolBiasAuditor(alpha=0.05)
results = auditor.analyze_tool_calls(traces_a, traces_b)
for r in results:
    print(f"{r.tool_name}: rate_a={r.invocation_rate_a:.2f}, "
          f"rate_b={r.invocation_rate_b:.2f}, p={r.p_value:.4f}")

RAGBiasAnalyzer

RAGBiasAnalyzer

Tests retrieval-augmented-generation pipelines for demographic-conditional bias. Compares the retrieved document sets and the generated outputs between two demographic groups; flags significant Jaccard distance in retrieval or divergence in outputs.

python
from vfairness.agents import RAGBiasAnalyzer

analyzer = RAGBiasAnalyzer()
result = analyzer.analyze(
    retrieved_docs_a=docs_for_group_a,
    retrieved_docs_b=docs_for_group_b,
    output_a=generated_for_group_a,
    output_b=generated_for_group_b,
)
print(result.retrieval_disparity, result.is_retrieval_biased)
print(result.output_disparity, result.is_output_biased)

PipelineTracker

PipelineTracker

Tracks bias at each stage of the agent pipeline (retrieval → reasoning → tool selection → action) and identifies which stage contributes the most bias.

python
tracker = PipelineTracker(["retrieval", "reasoning", "tool_selection", "action"])
tracker.record_stage("retrieval", outcomes_a, outcomes_b)
tracker.record_stage("reasoning", outcomes_a, outcomes_b)
tracker.record_stage("tool_selection", outcomes_a, outcomes_b)
tracker.record_stage("action", outcomes_a, outcomes_b)

results = tracker.compute_cumulative()
for r in results:
    print(f"{r.stage_name}: bias={r.cumulative_bias:.3f} "
          f"(+{r.stage_contribution:.3f} from this stage)")
print(f"Biggest source: {tracker.identify_bias_source()}")

TemporalTracker

TemporalTracker

Monitors how fairness changes over repeated interactions. Detects feedback loops using Kendall's tau and statistical drift using CUSUM and EWMA control charts.

python
tracker = TemporalTracker()
for turn in range(20):
    tracker.record_turn(turn, group_a_outcomes, group_b_outcomes)

# Three drift detection methods
tracker.detect_drift(threshold=0.1)               # Simple threshold
cusum = tracker.detect_drift_cusum(drift_limit=5)  # CUSUM change-point
ewma = tracker.detect_drift_ewma(span=5)           # EWMA control chart

# Feedback loop detection (Kendall's tau)
loop = tracker.detect_feedback_loop()
print(f"Feedback loop: {loop['has_feedback_loop']}")

ActionBiasAnalyzer

ActionBiasAnalyzer

Measures whether an agent's tangible actions (salary recommendations, approval rates, routing decisions) differ across demographics. Includes delegation pattern analysis.

python
analyzer = ActionBiasAnalyzer(alpha=0.05)

# Outcome disparity (Cohen's d + bootstrap CI)
result = analyzer.analyze_outcomes(actions_a, actions_b, outcome_field="salary")
print(f"Disparity: {result.disparity:.0f}, Effect: {result.effect_size:.2f}")

# Delegation routing patterns (chi-square)
delegation = analyzer.analyze_delegation(routing_a, routing_b)
print(f"Chi²={delegation['chi2_statistic']:.2f}, p={delegation['p_value']:.4f}")

Multi-Agent Fairness Testing

Multi-Agent Fairness Testing Overview

The vfairness.multi_agent module detects emergent bias in systems where multiple agents interact. Madigan et al. (2025) proved that system bias cannot be predicted from component bias — testing each agent separately is insufficient.

The Three Scenarios

Amplification: System bias exceeds component bias. Reduction: System bias is lower. Novel emergence: Components are unbiased but the system IS biased — the most dangerous case.

python
from vfairness.multi_agent import (
    CompositionalityAnalyzer,         # Component vs system bias
    GroupthinkDetector,               # Echo-chamber detection
    EmergentBiasDetector,             # Novel bias from interaction
    AdversarialCollusionDetector,     # Bias amplified by interaction (Khan et al. 2023)
    DelegationRoutingAuditor,         # Demographic-conditional routing (Bertrand & Mullainathan 2004)
    NegotiationFairnessTracker,       # Turn-by-turn fairness drift (Bianchi et al. 2024)
    MultiAgentRunHarness,             # Framework-agnostic trace capture
)

CompositionalityAnalyzer

CompositionalityAnalyzer

Compares each agent's bias score to the system's overall bias. Classifies the result into one of four scenarios. Supports both max-based and sum-based aggregation.

python
analyzer = CompositionalityAnalyzer()
result = analyzer.analyze(
    component_biases={"agent_A": 0.08, "agent_B": 0.05, "agent_C": 0.03},
    system_bias=0.25,
    aggregation_method="max",  # or "sum"
    threshold=0.05
)
print(f"Scenario: {result.scenario}")      # "amplification"
print(f"Divergence: {result.divergence:.3f}")
# Scenarios: "amplification" | "reduction" | "novel_emergence" | "consistent"

GroupthinkDetector

GroupthinkDetector

Detects whether agents converge on biased answers over multiple rounds of interaction (echo-chamber effect). Uses cosine similarity tracking, BFS coalition clustering, and a permutation test for statistical significance.

python
detector = GroupthinkDetector()
result = detector.analyze_convergence(agent_outputs_per_round=[
    {"agent_A": [0.3, 0.5], "agent_B": [0.7, 0.4]},  # Round 1: diverse
    {"agent_A": [0.5, 0.5], "agent_B": [0.5, 0.5]},  # Round 2: converged
])
print(f"Groupthink? {result.has_groupthink}")
print(f"p-value: {result.p_value:.4f}")  # Permutation test
print(f"Coalitions: {result.coalition_structure}")

EmergentBiasDetector

EmergentBiasDetector

Tests whether the full multi-agent system exhibits bias that can't be predicted from its parts. Uses bootstrap significance testing (500 resamples) to verify emergence is real, not noise.

python
import numpy as np
from vfairness.multi_agent import EmergentBiasDetector

detector = EmergentBiasDetector()
groups = np.array([0]*50 + [1]*50)
result = detector.analyze(
    component_outputs={"agent_A": comp_a, "agent_B": comp_b},
    system_outputs=system_out,
    groups=groups
)
print(f"Emergent? {result.is_emergent}")
print(f"Amplification: {result.amplification_factor:.2f}x")
print(f"p-value: {result.p_value:.4f}")

Agent Result Containers

Agent Result Containers

All vfairness.agents testers return dataclass result objects that carry RunMetadata and serialize via SerializableMixin (to_dict() / to_json()).

ClassReturned byKey attributes
CorrespondenceResultCorrespondenceTestertest_id, artifact_type, demographic_a, demographic_b, outcome_a, outcome_b, disparity_metric, sample_size, is_significant, p_value
ToolBiasResultToolBiasAuditortool_name, invocation_rate_a, invocation_rate_b, disparity_ratio, is_significant, p_value (chi-square / Fisher), confidence_interval
RAGBiasResultRAGBiasAnalyzerquery_id, demographic_a, demographic_b, retrieval_disparity (Jaccard distance), output_disparity, is_retrieval_biased, is_output_biased
StageResultPipelineTrackerstage_name, bias_metrics, cumulative_bias, stage_contribution (marginal contribution to bias)
TrajectoryResultTemporalTrackersession_id, turn_number, metric_name, value, cumulative_drift (absolute change from initial)
ActionBiasResultActionBiasAnalyzeraction_type, outcome_a, outcome_b, disparity, effect_size (Cohen’s d), is_significant, confidence_interval

Multi-Agent Result Containers

Multi-Agent Result Containers

Result objects returned by vfairness.multi_agent detectors. All carry RunMetadata and serialize via SerializableMixin.

ClassReturned byKey attributes
CompositionalityResultCompositionalityAnalyzer.analyzecomponent_scores, system_score, divergence (system vs. aggregate), scenario ∈ {amplification, reduction, novel_emergence, consistent}
GroupthinkResultGroupthinkDetector.analyzeconvergence_trajectory (per-round 0=diverse, 1=identical), has_groupthink, coalition_structure, echo_chamber_score, p_value, is_significant
EmergentBiasResultEmergentBiasDetector.analyzesystem_bias, max_component_bias, amplification_factor, is_emergent, p_value, is_significant
CollusionResultAdversarialCollusionDetector.analyzepre_bias_per_agent, post_bias_per_agent, pre_mean_bias, post_mean_bias, collusion_score (post − pre), is_collusion, p_value (sample-level paired permutation), is_significant
DelegationResultDelegationRoutingAuditor.analyzecontingency_table, routes, groups, chi_square, odds_ratio, cramers_v (effect size), p_value, test_used ∈ {fisher, chi2}, per_route_disparity, is_significant
NegotiationResultNegotiationFairnessTracker.analyzeper_turn_disparity, mean_disparity, max_disparity, final_minus_initial, mann_kendall_tau, p_value, trend ∈ {widening, narrowing, stable}, is_widening, is_significant
HarnessTraceMultiAgentRunHarness.tracesample_groups, component_outputs, system_outputs, pre_interaction_outputs, post_interaction_outputs, routing_decisions, routing_demographics, per_turn_group_a, per_turn_group_b, n_samples

AdversarialCollusionDetector

AdversarialCollusionDetector

Detects bias amplified by inter-agent interaction rather than already present in individual agents. Each agent's group-conditional output disparity is measured before and after the interaction round; a systematic increase post-interaction is the collusion signature.

Methodology grounding: Khan et al. (2023) Debating with More Persuasive LLMs; Du et al. (2023) Multi-Agent Debate; Bianchi et al. (2024) Cooperation, Competition & Maliciousness. Significance uses a paired sample-level permutation test (default 500 perms) for high power even with few agents.

python
from vfairness.multi_agent import AdversarialCollusionDetector
import numpy as np

detector = AdversarialCollusionDetector(alpha=0.05, n_permutations=500, random_seed=42)
groups = np.array([0]*50 + [1]*50)
pre = {"agent_a": pre_a_outputs, "agent_b": pre_b_outputs}
post = {"agent_a": post_a_outputs, "agent_b": post_b_outputs}
result = detector.analyze(pre, post, groups)
print(result.is_collusion, result.collusion_score, result.p_value)

DelegationRoutingAuditor

DelegationRoutingAuditor

Tests whether an orchestrator routes demographically equivalent tasks to different downstream agents. Generalizes the Bertrand & Mullainathan (2004) correspondence-test methodology from binary callbacks to categorical routing decisions. Reports Cramér's V as the effect size alongside the p-value, so trivial disparities in huge samples don't masquerade as audit failures.

Statistical test: 2 × 2 routes × groups → Fisher's exact (default); larger tables → chi-square test of independence.

python
from vfairness.multi_agent import DelegationRoutingAuditor

auditor = DelegationRoutingAuditor(alpha=0.05)
routes = ["senior"]*40 + ["junior"]*40
demographics = ["A"]*40 + ["B"]*40
result = auditor.analyze(routes, demographics)
print(result.test_used, result.p_value, result.cramers_v)
print(result.per_route_disparity)

NegotiationFairnessTracker

NegotiationFairnessTracker

Per-turn fairness tracker for multi-turn dialogues. Computes the per-turn group-conditional outcome disparity series, then runs a Mann-Kendall trend test (non-parametric) to detect drift — systematic widening or narrowing of the gap across turns. End-of-dialogue metrics alone cannot distinguish a stable non-zero gap from a gap that grew steadily over the negotiation.

Methodology grounding: Davidson et al. (2024) MultiAgentBench; Bianchi et al. (2024).

python
from vfairness.multi_agent import NegotiationFairnessTracker

tracker = NegotiationFairnessTracker(alpha=0.05)
# Per-turn outcome for each demographic (e.g., offered price each turn).
result = tracker.analyze(
    group_a_outcomes_per_turn=[100, 90, 80, 70, 60],
    group_b_outcomes_per_turn=[100, 99, 98, 97, 96],
)
print(result.trend, result.mann_kendall_tau, result.is_widening)

MultiAgentRunHarness

MultiAgentRunHarness

Framework-agnostic capture surface that sits between user code (autogen / crewai / langgraph / raw function calls) and the analyzers. Closes the “bring your own arrays” gap by giving a structured way to record per-sample, per-routing, and per-turn observations and adapt them to the analyzer input shapes via as_emergent_inputs(), as_compositionality_input(), as_collusion_inputs(), as_delegation_inputs(), and as_negotiation_inputs().

python
from vfairness.multi_agent import MultiAgentRunHarness, EmergentBiasDetector

with MultiAgentRunHarness() as h:
    for sample in dataset:
        agent_outs = run_agents(sample)   # your framework here
        sys_out = orchestrator(sample)
        h.record_sample(
            group=sample.protected_group,
            component_outputs=agent_outs,
            system_output=sys_out,
            pre_interaction=run_agents_in_isolation(sample),
            post_interaction=agent_outs,
        )

# Now feed any analyzer.
comp_out, sys_out, groups = h.as_emergent_inputs()
EmergentBiasDetector().analyze(comp_out, sys_out, groups)

Rendering Adapters

Rendering Overview

The vfairness.rendering module turns any result object (or plain dict) into a self-contained, standards-compliant SVG via Jinja2 templates. Adapters are pure functions: in → report-or-dict, out → SVG string (or saved file if save_path is given). 43 templates, ~45 public adapter functions across 15 adapter modules.

Common signature

Every adapter follows the same shape: name_to_svg(report_or_dict, *, explanation: str = "", save_path: Optional[str] = None) -> str. Pass a vfairness result object or its .to_dict() output. Returns the SVG source as a string; if save_path is set, also writes the file. Visit the SVG Gallery for live previews of every template.

python
from vfairness.rendering import (
    bias_audit_to_svg, fairness_report_to_svg, calibration_report_to_svg,
    intersectional_disparity_to_svg, pareto_frontier_to_svg, report_card_to_svg,
    # ... and ~40 more (see table below)
)

# Render a bias audit report to SVG and save
svg = bias_audit_to_svg(
    report,
    explanation="Q4 2026 audit of the lending model",
    save_path="reports/q4_audit.svg",
)

Fairness & bias adapters

Fairness & bias adapters

AdapterRenders
bias_audit_to_svgBiasDetector report — representation, statistical disparity, proxy risk modules
fairness_report_to_svgCompact metric-card report from a fairness analysis dict
fairness_detailed_report_to_svgFull multi-section fairness report with sub-cards and explanations
metrics_bar_chart_to_svgSide-by-side bar chart of fairness metrics across groups
radar_chart_to_svgPolar/radar visualization of multi-metric fairness profile
group_comparison_to_svgGroup-vs-group outcome comparison panel
disparity_heatmap_to_svgHeatmap of pairwise group disparities
intersectional_analysis_to_svgIntersectional fairness analysis: per-intersection bars + significance
intersectional_disparity_to_svgRanked bar chart of intersectional disparities (most disadvantaged first)
proxy_risk_to_svgProxy-feature risk scores from the bias detector
correlation_heatmap_to_svgCorrelation heatmap of features against protected attribute
correlation_matrix_to_svgFull correlation matrix variant with optional method labels

Calibration & threshold adapters

Calibration & threshold adapters

AdapterRenders
calibration_report_to_svgReliability diagrams + ECE / Brier / NLL summary per group
calibration_disparity_to_svgGroup-vs-group calibration gap visualization
group_calibration_to_svgPer-group calibration curves with shared axes
reliability_diagram_to_svgStandalone reliability diagram with confidence bands
threshold_optimization_to_svgPer-group operating-point selection plot
tradeoff_analysis_to_svgFairness-accuracy trade-off curve

Training & modeling adapters

Training & modeling adapters

AdapterRenders
training_report_to_svgTraining-time intervention summary (loss curves, fairness metrics)
training_analysis_report_to_svgExtended training report with per-epoch fairness/accuracy
method_comparison_to_svgSide-by-side comparison of multiple training methods on the Pareto curve
reweighting_comparison_to_svgPre/post-reweighting outcome distributions per group
transformation_comparison_to_svgPre/post feature-transformation distributions
regression_fairness_to_svgRegression-specific fairness panel (MAE/MSE parity, residual distributions)
ranking_fairness_to_svgRanking-specific fairness (exposure parity, rank disparities)

Monitoring, drift & experimentation adapters

Monitoring, drift & experimentation adapters

AdapterRenders
monitoring_dashboard_to_svgFull fairness monitoring dashboard with KPI tiles and trend lines
drift_report_to_svgDrift-detection report with severity-colored badges
alert_timeline_to_svgTime-ordered alert timeline (prioritized by FairnessAlertPrioritizer)
temporal_analysis_to_svgTemporal fairness trend with CUSUM/EWMA overlays
experiment_results_to_svgA/B experiment outcomes with per-arm fairness metrics
experiment_recommendation_to_svgAutomated deploy/hold/revert recommendation card
power_analysis_to_svgPer-intersection statistical power chart
pareto_frontier_to_svgMulti-objective Pareto frontier with non-dominated markers
causal_decomposition_to_svgNDE / NIE mediation decomposition (proportion mediated)

Workflow, reporting & misc adapters

Workflow, reporting & misc adapters

AdapterRenders
workflow_overview_to_svg11-pillar pipeline diagram (the homepage hero)
cicd_pipeline_to_svgCI/CD gate visualization (pass/fail per stage)
hierarchical_gate_to_svgHierarchical fairness-gate decision tree
report_card_to_svgFairnessReportCard render (model card-style summary)
reporting_dashboard_to_svgEnd-of-period reporting dashboard with KPI tiles
data_validation_to_svgDataBiasValidator output (data quality + bias warnings)
auto_discovery_to_svgAutomated bias discovery overview
robustness_testing_to_svgRobustness test panel (perturbation impact on fairness metrics)
confidence_intervals_to_svgBootstrap / Bayesian credible-interval visualization
effect_sizes_to_svgEffect-size summary chart with interpretation labels

All adapters use the shared template loader and the same Jinja2 filter set (f1-f4, pct, pct1, truncate_text) and inherit definitions from _shared_defs.svg (logo, icon, gradients).