Overview
This walkthrough demonstrates a complete fairness audit of a computer vision model used in healthcare. Unlike tabular classifiers or black-box LLMs, image-based AI systems present unique fairness challenges: demographic representation in training images, performance disparities across skin tones and anatomical variations, and regression-style continuous outputs (urgency scores) alongside binary triage decisions.
By following this guide, you will produce: a validated imaging dataset audit, bias detection across demographic groups, feature attribution analysis, a calibrated triage model, comprehensive fairness metrics with confidence intervals, robustness tests across imaging conditions, CI/CD gate decisions, monitoring dashboards, multi-tier reports, and an EU AI Act–compliant model card for a healthcare AI system — all backed by 23 production-grade SVG artifacts.
The Scenario
Chest X-Ray Triage Model — EU AI Act High-Risk System
A hospital network deploys a deep learning model (DenseNet-121) to prioritize chest X-rays in the emergency department. The model outputs an urgency score (0–100) and a binary triage decision (urgent/routine). Under the EU AI Act (Annex III, Section 5a), AI systems intended to be used for medical diagnosis or triage are classified as high-risk AI, subject to the most stringent compliance requirements.
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
# ── Load imaging metadata + model predictions ──
# Each row = one chest X-ray with radiologist labels + model predictions
df = pd.read_csv("cxr_triage_predictions.csv")
# Columns: study_id, urgency_score (0-100), triage_decision (urgent/routine),
# radiologist_label, gender, race, age_group, imaging_site, device_type
PROTECTED = ["gender", "race", "age_group"]
SCORE_COL = "urgency_score"
OUTCOME_COL = "radiologist_label" # Ground truth from radiologist panel
FEATURES = ["imaging_site", "device_type", "study_hour", "body_region"]
# Binary labels
df["urgent_true"] = (df[OUTCOME_COL] == "urgent").astype(int)
df["urgent_pred"] = (df["triage_decision"] == "urgent").astype(int)
y_true = df["urgent_true"]
y_pred = df["urgent_pred"]
y_scores = df[SCORE_COL] / 100.0 # Normalize to [0, 1]
sensitive = df[PROTECTED]
# Train/test split (temporal: older studies for train, recent for test)
df_train = df[df["study_date"] < "2025-01-01"]
df_test = df[df["study_date"] >= "2025-01-01"]
print(f"Dataset: {len(df)} X-rays ({len(df_train)} train, {len(df_test)} test)")
print(f"Urgent prevalence: {df['urgent_true'].mean():.1%}")
print(f"Protected attributes: {PROTECTED}")
Audit Pipeline
The ten phases below follow the standard vfairness pipeline but address image-specific challenges: representation in visual data, feature attribution for convolutional networks, calibration of continuous urgency scores, and regression fairness across demographic and clinical subgroups.
Validation
Detection
Analysis
Training
Testing
Gating
Card
Data Validation
Validate the imaging dataset for demographic representation, acquisition site diversity, and outcome label quality. Medical imaging datasets frequently suffer from site-specific bias (images from different hospitals have different visual characteristics) and underrepresentation of minority populations.
from vfairness.operations.cicd import DataBiasValidator, DataValidationConfig
validator = DataBiasValidator(
protected_attributes=PROTECTED,
config=DataValidationConfig(
min_samples_per_group=100, # Higher minimum for medical imaging
missing_value_threshold=0.02, # Strict: clinical data must be complete
),
)
result = validator.validate(df, outcome_column="urgent_true")
print(f"Passed: {result.passed}")
print(f"Issues: {len(result.issues)}")
# ── Generate SVG artifact ──
from vfairness.rendering import data_validation_to_svg
data_validation_to_svg(
result,
explanation="Imaging dataset validation for the chest X-ray triage model. Checks "
"demographic representation across 6 imaging sites, device type distribution, "
"missing metadata rates, and outcome label quality.",
save_path="artifacts/01_data_validation.svg",
)
1 critical: race=Asian underrepresented at 4.2% (population base rate ~6%). 1 warning: imaging site #3 contributes 38% of images (site-specific bias risk). 1 informational: 0.8% of images have missing age_group metadata. Proceed with documented caveats and reweighting.
Bias Detection & Auto-Discovery
Scan the model’s predictions for demographic disparities. In medical imaging, bias can arise from: (1) prevalence differences across populations, (2) image quality variations by site/device, and (3) annotation bias from radiologist panels with limited diversity.
from vfairness.evaluation.vfairness_metrics import (
detect_protected_attributes,
scan_fairness_violations,
discover_intersectional_groups,
)
from vfairness.preprocessing.bias_detection import BiasDetector
from vfairness.rendering import auto_discovery_to_svg, bias_audit_to_svg
# ── Auto-discovery on model predictions ──
candidates = detect_protected_attributes(df)
violations = scan_fairness_violations(df, y_pred.to_numpy(), y_true=y_true.to_numpy())
intersectional = discover_intersectional_groups(df, PROTECTED, y_pred.to_numpy())
auto_discovery_to_svg(
candidates=candidates,
violations=violations,
group_advantages=intersectional,
explanation="Automated scan of the chest X-ray triage model identifying gender, race, "
"and age_group as attributes correlated with prediction disparities. "
"age_group shows highest correlation due to disease prevalence differences.",
save_path="artifacts/02a_auto_discovery.svg",
)
# ── Full bias audit ──
detector = BiasDetector(df, protected_attributes=PROTECTED, outcome_column="urgent_true")
audit = detector.full_audit()
bias_audit_to_svg(
audit,
explanation="Comprehensive bias audit of the triage model covering outcome disparities, "
"sensitivity/specificity by group, and intersectional analysis. Focus on "
"clinically significant false negative rate differences.",
save_path="artifacts/02b_bias_audit.svg",
)
False negative rate disparity: race=Black patients have 12.4% FNR vs 6.1% overall — urgent cases are more likely to be missed. Intersectional group Black × age_group=65+ has the highest FNR (15.8%). Two high-severity violations flagged for immediate review.
Feature & Metadata Analysis
Analyze correlations between metadata features (imaging site, device type, acquisition parameters) and both outcomes and protected attributes. In medical imaging, site-specific visual signatures can act as demographic proxies — if certain demographics are concentrated at specific hospitals.
from vfairness.preprocessing.feature_engineering import FeatureEngineeringAnalyzer
from vfairness.rendering import (
proxy_risk_to_svg,
correlation_heatmap_to_svg,
)
analyzer = FeatureEngineeringAnalyzer(
df, protected_attributes=PROTECTED,
target_column="urgent_true", feature_columns=FEATURES,
)
report = analyzer.full_analysis()
# ── Proxy risk: imaging_site as demographic proxy ──
proxy_risk_to_svg(
report.proxy_variables,
explanation="Proxy analysis for imaging metadata. 'imaging_site' can act as a "
"demographic proxy because hospital catchment areas are demographically "
"stratified.",
save_path="artifacts/03a_proxy_risk.svg",
)
# ── Correlation heatmap ──
# correlation_heatmap_to_svg expects a feature-keyed view of the matrix
from types import SimpleNamespace
cm = report.correlation_matrix
heatmap_view = SimpleNamespace(
features=cm.feature_names,
protected_attributes=cm.protected_attributes,
correlations={
f: {a: float(cm.correlations[a][f]) for a in cm.protected_attributes}
for f in cm.feature_names
},
)
correlation_heatmap_to_svg(
heatmap_view,
explanation="Metadata-to-demographic correlation matrix. Reveals how imaging acquisition "
"context (site, device, time-of-day) correlates with protected attributes.",
save_path="artifacts/03b_correlation_heatmap.svg",
)
imaging_site is a critical proxy for race (r=0.68). Site #3 (38% of images) is 82% White patients, while site #5 (8% of images) is 71% Black patients. This demographic stratification by site means the model may learn site-specific visual artifacts as demographic shortcuts.
Fair Training with Sample Reweighting
Retrain the model using sample reweighting to correct for demographic imbalance and site-specific bias. For imaging models, fairness-aware training typically uses reweighting (upweighting underrepresented group-outcome combinations) rather than adversarial methods, which can destabilize visual feature learning.
from vfairness.preprocessing.feature_engineering import ReweightingTransformer
from vfairness.post_processing.reweighting import ReweightingAnalyzer
from vfairness.rendering import (
training_report_to_svg,
reweighting_comparison_to_svg,
)
# ── Kamiran & Calders sample weights for fairness-aware retraining ──
reweighter = ReweightingTransformer(
protected_attributes=PROTECTED, method="target_parity",
)
reweighter.fit(df_train, y=df_train["urgent_true"])
weights = reweighter.get_sample_weights(df_train, y=df_train["urgent_true"])
# Retrain DenseNet-121 with sample weights
# model.fit(X_train_images, y_train, sample_weight=weights)
# ... training loop produces training_history dict, rendered with
# training_report_to_svg(training_history, save_path="artifacts/04a_training_report.svg") ...
# ── Analyze what prediction reweighting would change ──
rw_report = ReweightingAnalyzer(y_true, y_scores, sensitive["race"]).full_analysis()
reweighting_comparison_to_svg(
rw_report,
explanation="Reweighting impact analysis: per-group positive rates before and "
"after reweighting, and the fairness-accuracy trade-off.",
save_path="artifacts/04b_reweighting_comparison.svg",
)
Reweighting reduces the FNR disparity for race=Black from 12.4% to 7.8% (threshold: 8%). Overall AUC drops marginally from 0.912 to 0.905. The trade-off is clinically acceptable — fewer missed urgent cases in underserved populations.
Urgency Score Calibration
The model’s urgency scores (0–100) must be well-calibrated so that a score of “80” means the same clinical urgency regardless of patient demographics. In healthcare, miscalibration has direct patient safety implications.
from vfairness.post_processing.calibration import CalibrationAnalyzer
from vfairness.rendering import (
calibration_report_to_svg,
reliability_diagram_to_svg,
)
cal = CalibrationAnalyzer(y_true, y_scores, sensitive["race"])
cal_report = cal.full_analysis()
calibration_report_to_svg(
cal_report,
explanation="Group-wise calibration analysis for urgency scores. ECE computed per "
"racial group to detect if score '80' means different urgency levels "
"for different populations — a direct patient safety concern.",
save_path="artifacts/05a_calibration_report.svg",
)
reliability_diagram_to_svg(
y_true, y_scores,
explanation="Reliability diagram for the triage urgency scores. Deviation from the "
"diagonal indicates systematic over- or under-estimation of urgency.",
save_path="artifacts/05b_reliability_diagram.svg",
)
After reweighted training: overall ECE = 0.042. Group ECE disparity reduced from 0.051 to 0.018 (threshold: 0.02). Isotonic calibration applied as a final correction, bringing all groups below threshold.
Comprehensive Metrics & Robustness Testing
Evaluate the reweighted and calibrated model across all fairness dimensions. For healthcare AI, equalized odds (equal sensitivity/specificity across groups) is typically the most clinically relevant metric, since both false positives (unnecessary escalation) and false negatives (missed urgent cases) carry clinical consequences.
from vfairness import FairnessAnalyzer
from vfairness.evaluation.vfairness_metrics import (
permutation_test, sensitivity_analysis, subgroup_robustness_audit,
)
from vfairness.evaluation.vfairness_metrics.regression import get_group_metrics
from vfairness.rendering import (
radar_chart_to_svg,
metrics_bar_chart_to_svg,
confidence_intervals_to_svg,
group_comparison_to_svg,
robustness_testing_to_svg,
regression_fairness_to_svg,
)
analyzer = FairnessAnalyzer(
y_true, y_pred, sensitive["race"],
y_prob=y_scores, fair_explainer=True,
)
report = analyzer.get_report(include_ci=True, include_explanations=True)
radar_chart_to_svg(report,
explanation="Multi-metric radar chart for the chest X-ray triage model. "
"Equalized odds is the primary clinical fairness metric.",
save_path="artifacts/06a_radar_chart.svg")
metrics_bar_chart_to_svg(report,
explanation="All fairness metrics against healthcare-specific thresholds. "
"FNR parity is stricter (0.05) due to patient safety requirements.",
save_path="artifacts/06b_metrics_bar_chart.svg")
# The forest plot reads a "confidence_intervals" mapping with lower/upper keys,
# built here from the report's metrics_with_ci entries.
confidence_intervals_to_svg(
{
"metrics": report["metrics"],
"confidence_intervals": {
m: {"lower": ci["lower_bound"], "upper": ci["upper_bound"]}
for m, ci in report["metrics_with_ci"].items()
},
},
explanation="95% bootstrap confidence intervals for each metric.",
save_path="artifacts/06c_confidence_intervals.svg")
group_comparison_to_svg(report,
explanation="Per-group triage rate comparison.",
save_path="artifacts/06d_group_comparison.svg")
# ── Robustness testing ──
# The robustness tests take a metric function of (y_pred, sensitive_attr)
def parity_gap(y_pred_arr, attr):
rates = [np.mean(y_pred_arr[attr == g]) for g in np.unique(attr)]
return max(rates) - min(rates)
race_arr = sensitive["race"].to_numpy()
perm = permutation_test(y_pred.to_numpy(), race_arr, parity_gap)
sens = sensitivity_analysis(y_pred.to_numpy(), race_arr, parity_gap)
audit = subgroup_robustness_audit(y_pred.to_numpy(), sensitive, y_true=y_true.to_numpy())
robustness_testing_to_svg(
permutation_results=[perm], sensitivity_results=[sens],
subgroup_audit=audit,
explanation="Robustness validation across imaging conditions (site, device, time). "
"Tests metric stability under data perturbation.",
save_path="artifacts/06e_robustness_testing.svg")
# ── Regression fairness on urgency scores ──
regression_fairness_to_svg(
group_metrics=get_group_metrics(y_true.astype(float), y_scores, sensitive["race"]),
explanation="Regression fairness analysis of continuous urgency scores. Evaluates MAE "
"parity, mean prediction difference, and residual bias across racial groups.",
save_path="artifacts/06f_regression_fairness.svg")
After reweighting + calibration: equalized odds difference = 0.04 (pass), FNR parity = 0.03 (pass, healthcare threshold 0.05), demographic parity difference = 0.07 (pass). Regression MAE parity = 0.08 (pass). All permutation tests significant (p < 0.001). Robustness score: 0.81 (PASS).
CI/CD Gating
Medical AI deployments require stringent gates. The fairness gate is complemented by clinical performance gates (minimum sensitivity, maximum FNR) that must be satisfied per demographic group before deployment.
from vfairness.operations.cicd import ModelFairnessGate, GateConfig
from vfairness.operations.cicd.gate import HierarchicalGateConfig
from vfairness.rendering import cicd_pipeline_to_svg, hierarchical_gate_to_svg
gate = ModelFairnessGate(config=GateConfig(
thresholds={
"demographic_parity_difference": 0.10,
"equalized_odds_difference": 0.08, # Stricter for healthcare
"equal_opportunity_difference": 0.05, # FNR parity — patient safety
},
))
decision = gate.evaluate(y_true, y_pred, sensitive["race"])
cicd_pipeline_to_svg(gate_decision=decision,
explanation="CI/CD fairness gate for the chest X-ray triage model. Healthcare-grade "
"thresholds: stricter equalized odds (0.08) and FNR parity (0.05).",
save_path="artifacts/07a_cicd_pipeline.svg")
hier_config = HierarchicalGateConfig(
check_intersections=True,
intersection_depth=2,
min_group_size=50,
per_intersection_thresholds={
"Black_65+": {"equal_opportunity_difference": 0.08},
},
)
hier_decision = gate.evaluate_hierarchical(
y_true.to_numpy(), y_pred.to_numpy(),
protected_attrs={"race": sensitive["race"].to_numpy(),
"age_group": sensitive["age_group"].to_numpy()},
hierarchical_config=hier_config,
)
hierarchical_gate_to_svg(hier_decision,
explanation="Hierarchical gate with clinical-grade intersectional evaluation. "
"Elevated thresholds for previously identified vulnerable group (Black × 65+).",
save_path="artifacts/07b_hierarchical_gate.svg")
All global and per-group thresholds satisfied after reweighting. The Black × 65+ subgroup passes its elevated threshold (FNR parity = 0.04, threshold 0.08). One SmallSampleWarning for Asian × 18-24 (n=58).
Production Monitoring
Medical imaging models face unique drift sources: new imaging equipment, seasonal disease patterns, and changes in patient population demographics. Monitoring must track both fairness metrics and clinical performance per subgroup.
from vfairness.operations.monitoring import FairnessMonitor, FairnessMonitorConfig
from vfairness.operations.monitoring import FairnessDriftDetector
from vfairness.rendering import monitoring_dashboard_to_svg, drift_report_to_svg
monitor = FairnessMonitor(config=FairnessMonitorConfig(
window_size=1000, # rows retained in the sliding window
alert_threshold=0.8, # minimum acceptable disparate impact ratio
metrics_to_track=["disparate_impact", "demographic_parity", "equalized_odds"],
))
# Batches are DataFrames carrying the prediction, the label, and the
# protected-attribute columns (in production this runs on every batch)
reference_df = pd.DataFrame({
"prediction": y_pred.to_numpy(),
"label": y_true.to_numpy(),
"race": df["race"].to_numpy(),
})
# stand-in for the next batch of triage decisions: same columns, fresh rows
production_batch_df = reference_df.sample(500, random_state=7)
monitor.set_reference(reference_df)
window = monitor.update_and_check(production_batch_df)
monitoring_dashboard_to_svg(window,
explanation="Fairness monitoring snapshot for the triage model: tracked metrics, "
"per-group rates, and alert status. Alerts on new imaging equipment "
"or seasonal population shifts.",
save_path="artifacts/08a_monitoring_dashboard.svg")
# ── Drift detection ──
# The detector compares series of a fairness metric over time, in production
# taken from monitor.get_metric_history(); illustrative values shown here
baseline_metric_series = pd.Series([0.04, 0.03, 0.05, 0.04, 0.03, 0.04, 0.05, 0.04] * 8)
production_metric_series = pd.Series(np.linspace(0.04, 0.10, 64))
detector = FairnessDriftDetector()
detector.set_baseline(baseline_metric_series)
drift = detector.check_drift(production_metric_series, metric="equal_opportunity")
drift_report_to_svg(drift,
explanation="Drift analysis comparing production metrics against audit baseline. "
"Monitors for equipment-related drift and seasonal population changes.",
save_path="artifacts/08b_drift_report.svg")
No significant drift detected. FNR parity stable at 0.03 ± 0.01. Automated alerts configured for equipment changes (new device types trigger mandatory re-evaluation) and seasonal thresholds.
Multi-Tier Reporting
Generate three report tiers. The executive report for hospital leadership emphasizes patient safety metrics. The operational report for radiology teams includes site-specific performance. The technical report provides full statistical evidence for regulatory submission.
from vfairness.operations.reporting import (
ReportGenerator, ReportConfig, MetricsStore, ReportTier, OutputFormat,
)
from vfairness.rendering import reporting_dashboard_to_svg
store = MetricsStore()
store.ingest_window_metrics(window) # Phase 8 monitoring window
store.ingest_from_monitor(monitor) # full monitor history
store.ingest_drift_result(drift)
generator = ReportGenerator(store, config=ReportConfig(
custom_title="Chest X-Ray Triage Model Fairness Report",
))
exec_report = generator.generate(tier=ReportTier.EXECUTIVE, output_format=OutputFormat.MARKDOWN)
reporting_dashboard_to_svg(exec_report, tier="executive",
explanation="Hospital leadership summary: patient safety metrics, FNR parity status, "
"and compliance for CE marking under the EU AI Act / MDR.",
save_path="artifacts/09a_executive_dashboard.svg")
ops_report = generator.generate(tier=ReportTier.OPERATIONAL, output_format=OutputFormat.MARKDOWN)
reporting_dashboard_to_svg(ops_report, tier="operational",
explanation="Radiology team dashboard: per-site performance, device-specific metrics, "
"and recommendations for imaging protocol adjustments.",
save_path="artifacts/09b_operational_dashboard.svg")
tech_report = generator.generate(tier=ReportTier.TECHNICAL, output_format=OutputFormat.JSON)
reporting_dashboard_to_svg(tech_report, tier="technical",
explanation="Full statistical evidence for regulatory submission. Includes bootstrap "
"CIs, regression fairness, and subgroup robustness audit results.",
save_path="artifacts/09c_technical_dashboard.svg")
EU AI Act Model Card (Healthcare)
Healthcare AI systems have dual regulatory obligations: the EU AI Act (Annex III, Section 5a) for high-risk AI, and the Medical Device Regulation (MDR 2017/745) when the system qualifies as a medical device. The model card must address both frameworks.
from vfairness.operations.cicd.gate import FairnessReportCard
from vfairness.rendering import report_card_to_svg
# The card is built from the hierarchical gate decision produced in Phase 7
card = FairnessReportCard(hier_decision, model_name="CXR-Triage-DenseNet-v3.2")
markdown = card.to_markdown()
payload = card.to_github_comment_payload()
# The SVG renderer works from the standard gate decision of Phase 7
report_card_to_svg(
decision=decision,
model_name="CXR-Triage-DenseNet-v3.2",
explanation="Fairness report card for the chest X-ray triage system: gate status "
"and the per-metric evaluations behind it. Accompanies the clinical "
"narrative documentation described below.",
save_path="artifacts/10_model_card.svg")
The card carries the measured evidence: gate status, per-level and intersectional results, and the model name. The narrative half of an EU AI Act model card is documentation you write around it, not something the library can compute. For this audit that narrative records: intended use (prioritization of chest X-rays in the emergency department workflow, radiologist review of every triage decision within four hours, a prioritization aid rather than a diagnostic tool); risk classification HIGH under EU AI Act Annex III together with the Medical Device Regulation (EU) 2017/745; the training data summary (multi-site hospital network, six imaging centers, sample reweighting applied for demographic balance, site-aware augmentation); the evaluation summary (temporal train/test split, bootstrap confidence intervals at the 95% level, quarterly radiologist panel review); human oversight (board-certified radiologist review, monthly fairness review by the clinical AI committee, documented and audited overrides); and limitations (single geographic region, underrepresented Asian population, pediatric X-rays excluded, portable/bedside studies possibly out of distribution, site-specific visual artifacts).
Healthcare AI systems face overlapping regulations:
- EU AI Act (Annex III, 5a) — High-risk AI classification for medical device AI systems
- MDR 2017/745 — CE marking requirements for Software as a Medical Device (SaMD)
- Article 9 — Risk management integrates with ISO 14971 (medical device risk management)
- Article 10 — Data governance aligns with GSPR Annex I requirements for clinical evidence
- Article 15 — Accuracy/robustness requirements map to clinical performance evaluation
Complete Artifact Inventory
The table below lists every artifact generated during this medical imaging audit.
| Phase | Artifact | Function | Module |
|---|---|---|---|
| 1 | Data Validation Report | data_validation_to_svg() | Preprocessing |
| 2 | Auto-Discovery Scanner | auto_discovery_to_svg() | Preprocessing |
| 2 | Bias Audit Dashboard | bias_audit_to_svg() | Preprocessing |
| 3 | Proxy Risk Assessment | proxy_risk_to_svg() | Preprocessing |
| 3 | Correlation Heatmap | correlation_heatmap_to_svg() | Preprocessing |
| 4 | Training Report | training_report_to_svg() | Training |
| 4 | Reweighting Comparison | reweighting_comparison_to_svg() | Training |
| 5 | Calibration Report | calibration_report_to_svg() | Post-Processing |
| 5 | Reliability Diagram | reliability_diagram_to_svg() | Post-Processing |
| 6 | Fairness Radar Chart | radar_chart_to_svg() | Evaluation |
| 6 | Metrics Bar Chart | metrics_bar_chart_to_svg() | Evaluation |
| 6 | Confidence Intervals | confidence_intervals_to_svg() | Evaluation |
| 6 | Group Comparison | group_comparison_to_svg() | Evaluation |
| 6 | Robustness Testing | robustness_testing_to_svg() | Evaluation |
| 6 | Regression Fairness | regression_fairness_to_svg() | Evaluation |
| 7 | CI/CD Pipeline Gate | cicd_pipeline_to_svg() | Operations |
| 7 | Hierarchical Gate | hierarchical_gate_to_svg() | Operations |
| 8 | Monitoring Dashboard | monitoring_dashboard_to_svg() | Operations |
| 8 | Drift Report | drift_report_to_svg() | Operations |
| 9 | Executive Dashboard | reporting_dashboard_to_svg() | Reporting |
| 9 | Operational Dashboard | reporting_dashboard_to_svg() | Reporting |
| 9 | Technical Dashboard | reporting_dashboard_to_svg() | Reporting |
| 10 | EU AI Act Model Card | report_card_to_svg() | Reporting |
Workflow Integration
The audit above produces artifacts manually. To automate and enforce these checks in your development pipeline, vfairness provides workflow integration tools that plug directly into MLOps, CI/CD, version control, and testing infrastructure. For medical imaging, this is especially critical given MDR post-market surveillance requirements. See the Workflow Integration guide for full documentation.
1. Experiment Tracking
Log every model retraining’s fairness metrics to MLflow or Weights & Biases. This is essential for medical imaging models that must be revalidated after equipment changes, site additions, or algorithm updates.
# Option A: explicit logging after Phase 6
from vfairness import log_fairness_to_mlflow
import mlflow
with mlflow.start_run(run_name="chest-xray-densenet121-v2.3"):
logged = log_fairness_to_mlflow(report, prefix="xray_v2.3")
print(f"Logged {logged} metrics to MLflow")
# Option B: auto-logging decorator — wraps any evaluation function
from vfairness import auto_log_fairness
@auto_log_fairness(backend="wandb", prefix="xray")
def evaluate_imaging_model(model, X_test, y_test, sensitive):
y_pred = model.predict(X_test)
return y_pred, y_test, sensitive # triggers automatic fairness logging
2. Pre-Commit Hooks
Enforce documentation standards before code reaches the repository. For medical AI, this ensures fairness configs and model cards are always up-to-date — a key MDR requirement for technical documentation.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/validantai/vfairness
rev: v0.0.9 # pin to the vfairness release you install
hooks:
- id: vfairness-check-config # validates fairness JSON configs
- id: vfairness-check-model-card # ensures model cards include fairness sections
3. pytest Integration
Embed fairness assertions in your test suite. For medical imaging, test that diagnostic accuracy is equitable across demographic groups and acquisition sites — same checks as Phase 6, but automated on every commit.
import pytest
from vfairness import assert_fairness, FairnessTestSuite
# Test diagnostic fairness across patient demographics
def test_imaging_model_fairness():
assert_fairness(
y_true, y_pred, sensitive["race"],
metrics=["demographic_parity_difference", "equalized_odds_difference"],
thresholds={"demographic_parity_difference": 0.08, "equalized_odds_difference": 0.10},
)
# Full test suite with JUnit XML export for CI
suite = FairnessTestSuite(
protected_attributes=["gender", "race", "age_group"],
metrics=["demographic_parity_difference", "equalized_odds_difference"],
thresholds={"demographic_parity_difference": 0.08, "equalized_odds_difference": 0.10},
)
results = suite.test_predictions(
y_true, y_pred, sensitive["race"],
attr_name="race",
raise_on_failure=False, # record failures in the XML instead of raising
)
xml = suite.to_junit_xml() # attach to CI pipeline artifacts
4. CI/CD Automation & PR Comments
Gate decisions from Phase 7 can be posted directly to GitHub as check results and PR comments. For medical imaging, this prevents model updates from reaching production without fairness validation — aligning with MDR change control requirements.
# Post gate result as a GitHub Check (from Phase 7)
check_payload = gate.create_github_check(decision)
# → POST to /repos/{owner}/{repo}/check-runs
# Generate PR comment with full fairness report card
from vfairness import FairnessReportCard
card = FairnessReportCard(decision, model_name="ChestXRay-DenseNet121-v2.3")
comment_payload = card.to_github_comment_payload()
# → POST to /repos/{owner}/{repo}/issues/{pr_number}/comments
# .github/workflows/fairness-checks.yml
name: Medical Imaging Fairness Gate
on: [pull_request]
jobs:
fairness:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install vfairness
- run: python -m pytest tests/ -m fairness --junitxml=fairness-results.xml
- run: python scripts/evaluate_imaging_gate.py # runs gate + posts PR comment
Phase 6 (Metrics) → @auto_log_fairness + assert_fairness()
Phase 7 (CI/CD) → create_github_check() + GitHub Actions YAML
Phase 10 (Model Card) → FairnessReportCard.to_github_comment_payload()
Every commit → Pre-commit hooks validate configs + model cards
Equipment change → Re-run full suite + log to experiment tracker for MDR traceability
EU AI Act Compliance Checklist
Healthcare AI has the most stringent compliance requirements. Each item below maps to specific audit artifacts and addresses the overlap between the EU AI Act and the Medical Device Regulation.
This walkthrough used demo data for illustration. To apply this process to your own medical imaging model:
- Ensure your imaging dataset has linked demographic metadata (may require IRB approval)
- Validate representation across all acquisition sites and equipment types
- Set clinically appropriate thresholds (consult with clinical AI committees)
- Apply sample reweighting before training; do not rely solely on post-hoc calibration
- Schedule equipment-triggered re-evaluations and quarterly clinical validation panels
- Coordinate EU AI Act and MDR documentation into a single conformity assessment
Compare with the Credit Scoring Audit (tabular data) or the LLM Audit (black-box foundation model). Explore all 44 SVG templates in the SVG Gallery.