equiaudit 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- equiaudit/__init__.py +3 -0
- equiaudit/cli/__init__.py +7 -0
- equiaudit/cli/__main__.py +204 -0
- equiaudit/cli/evaluator.py +455 -0
- equiaudit/data/GermanCredit.csv +1001 -0
- equiaudit/data/UCI_Credit_Card.csv +30001 -0
- equiaudit/data/adult-all.csv +48843 -0
- equiaudit/data/adult-all_discretized.csv +48843 -0
- equiaudit/data/bank-additional.csv +41189 -0
- equiaudit/data/german.csv +1001 -0
- equiaudit/data/german_discretized.csv +1001 -0
- equiaudit/gui/__init__.py +113 -0
- equiaudit/gui/app.py +30 -0
- equiaudit/gui/config.py +89 -0
- equiaudit/gui/screens/main_page.py +32 -0
- equiaudit/gui/screens/new_evaluation.py +983 -0
- equiaudit/gui/screens/view_results.py +704 -0
- equiaudit/gui/state.py +36 -0
- equiaudit/gui/styles.py +183 -0
- equiaudit/gui/utils.py +130 -0
- equiaudit/gui/widgets/fairness.py +748 -0
- equiaudit/gui/widgets/results.py +342 -0
- equiaudit/gui/widgets/stage_display.py +650 -0
- equiaudit/main.py +161 -0
- equiaudit/models/__init__.py +32 -0
- equiaudit/models/agent_manager.py +256 -0
- equiaudit/models/agents/__init__.py +11 -0
- equiaudit/models/agents/base_agent.py +242 -0
- equiaudit/models/agents/conversational_agent.py +21 -0
- equiaudit/models/agents/data_analyst_agent.py +84 -0
- equiaudit/models/agents/function_caller_agent.py +86 -0
- equiaudit/models/agents/humanizer_agent.py +58 -0
- equiaudit/models/agents/summary_agent.py +69 -0
- equiaudit/models/clients/__init__.py +13 -0
- equiaudit/models/clients/base_client.py +89 -0
- equiaudit/models/clients/client_factory.py +88 -0
- equiaudit/models/clients/gemini_client.py +113 -0
- equiaudit/models/clients/ollama_client.py +68 -0
- equiaudit/models/clients/openrouter_client.py +135 -0
- equiaudit/models/config.yml +147 -0
- equiaudit/pipeline/__init__.py +9 -0
- equiaudit/pipeline/config.py +88 -0
- equiaudit/pipeline/pipeline.py +642 -0
- equiaudit/pipeline/pipeline_config.yml +81 -0
- equiaudit/pipeline/stage.py +102 -0
- equiaudit/pipeline/stages/__init__.py +25 -0
- equiaudit/pipeline/stages/base.py +86 -0
- equiaudit/pipeline/stages/dataset_loading.py +21 -0
- equiaudit/pipeline/stages/discretization.py +384 -0
- equiaudit/pipeline/stages/fairness.py +176 -0
- equiaudit/pipeline/stages/imbalance.py +122 -0
- equiaudit/pipeline/stages/mitigation.py +332 -0
- equiaudit/pipeline/stages/objective.py +34 -0
- equiaudit/pipeline/stages/pair_selection.py +116 -0
- equiaudit/pipeline/stages/quality.py +28 -0
- equiaudit/pipeline/stages/recommendations.py +45 -0
- equiaudit/pipeline/stages/sensitive.py +134 -0
- equiaudit/pipeline/utils.py +878 -0
- equiaudit/reporting/__init__.py +0 -0
- equiaudit/reporting/pdf_generator.py +645 -0
- equiaudit/tools/__init__.py +0 -0
- equiaudit/tools/bias_mitigation_tools.py +653 -0
- equiaudit/tools/discretization_tools.py +259 -0
- equiaudit/tools/fairness_tools.py +1037 -0
- equiaudit/tools/tool.py +6 -0
- equiaudit/tools/tool_manager.py +65 -0
- equiaudit-0.1.1.dist-info/METADATA +161 -0
- equiaudit-0.1.1.dist-info/RECORD +71 -0
- equiaudit-0.1.1.dist-info/WHEEL +5 -0
- equiaudit-0.1.1.dist-info/entry_points.txt +2 -0
- equiaudit-0.1.1.dist-info/top_level.txt +1 -0
equiaudit/__init__.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main(args: argparse.Namespace = None) -> int:
|
|
10
|
+
"""Main entry point for CLI."""
|
|
11
|
+
|
|
12
|
+
if args is None:
|
|
13
|
+
args = parse_args()
|
|
14
|
+
|
|
15
|
+
# Handle environment variable loading from .env file
|
|
16
|
+
if args.env_file:
|
|
17
|
+
load_env_file(args.env_file)
|
|
18
|
+
|
|
19
|
+
# Import after env loading to ensure API keys are available
|
|
20
|
+
from equiaudit.cli.evaluator import FairnessEvaluator
|
|
21
|
+
|
|
22
|
+
# Create evaluator
|
|
23
|
+
evaluator = FairnessEvaluator(
|
|
24
|
+
config_path=args.config,
|
|
25
|
+
output_dir=args.output,
|
|
26
|
+
model=args.model,
|
|
27
|
+
verbose=not args.quiet,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Verify mode
|
|
31
|
+
if args.verify:
|
|
32
|
+
result = evaluator.verify()
|
|
33
|
+
return 0 if result.success else 1
|
|
34
|
+
|
|
35
|
+
# Evaluate mode - requires data
|
|
36
|
+
if not args.data:
|
|
37
|
+
print("Error: --data is required for evaluation mode")
|
|
38
|
+
print("Use --verify to check configuration only")
|
|
39
|
+
return 1
|
|
40
|
+
|
|
41
|
+
# Run evaluation
|
|
42
|
+
result = evaluator.evaluate(
|
|
43
|
+
data=args.data,
|
|
44
|
+
target=args.target,
|
|
45
|
+
objective=args.objective,
|
|
46
|
+
sensitive_columns=args.sensitive.split(",") if args.sensitive else None,
|
|
47
|
+
generate_pdf=not args.no_pdf,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if result.success:
|
|
51
|
+
print("\n" + "=" * 60)
|
|
52
|
+
print("EVALUATION COMPLETE")
|
|
53
|
+
print("=" * 60)
|
|
54
|
+
print(f"Dataset: {result.dataset}")
|
|
55
|
+
print(f"Target: {result.target_column or 'auto-detected'}")
|
|
56
|
+
print(f"Report Dir: {result.report_dir}")
|
|
57
|
+
if result.pdf_path:
|
|
58
|
+
print(f"PDF Report: {result.pdf_path}")
|
|
59
|
+
if result.markdown_path:
|
|
60
|
+
print(f"MD Report: {result.markdown_path}")
|
|
61
|
+
print(f"Stages: {', '.join(result.stages_completed)}")
|
|
62
|
+
if result.warnings:
|
|
63
|
+
print(f"\nWarnings ({len(result.warnings)}):")
|
|
64
|
+
for w in result.warnings:
|
|
65
|
+
print(f" - {w}")
|
|
66
|
+
return 0
|
|
67
|
+
else:
|
|
68
|
+
print("\n" + "=" * 60)
|
|
69
|
+
print("EVALUATION FAILED")
|
|
70
|
+
print("=" * 60)
|
|
71
|
+
print(f"Error: {result.error}")
|
|
72
|
+
return 1
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def parse_args() -> argparse.Namespace:
|
|
76
|
+
"""Parse command-line arguments."""
|
|
77
|
+
|
|
78
|
+
parser = argparse.ArgumentParser(
|
|
79
|
+
prog="fairness-eval",
|
|
80
|
+
description="Evaluate datasets for fairness and bias issues.",
|
|
81
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
82
|
+
epilog="""
|
|
83
|
+
Examples:
|
|
84
|
+
# Basic evaluation
|
|
85
|
+
equiaudit --data adult-all.csv --target Income
|
|
86
|
+
|
|
87
|
+
# Verify configuration
|
|
88
|
+
equiaudit --verify
|
|
89
|
+
|
|
90
|
+
# Custom config and output
|
|
91
|
+
equiaudit --config my_config.yml --data data.csv --output ./reports
|
|
92
|
+
|
|
93
|
+
# Specify model and sensitive columns
|
|
94
|
+
equiaudit --data data.csv --model gemini-flash --sensitive "age,sex,race"
|
|
95
|
+
|
|
96
|
+
Environment Variables:
|
|
97
|
+
OPENROUTER_API_KEY API key for OpenRouter models
|
|
98
|
+
GOOGLE_API_KEY API key for Gemini models
|
|
99
|
+
""",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Data input
|
|
103
|
+
parser.add_argument(
|
|
104
|
+
"--data", "-d",
|
|
105
|
+
type=str,
|
|
106
|
+
help="Path to CSV dataset file",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
parser.add_argument(
|
|
110
|
+
"--target", "-t",
|
|
111
|
+
type=str,
|
|
112
|
+
default=None,
|
|
113
|
+
help="Target column name for classification (auto-detected if not specified)",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
parser.add_argument(
|
|
117
|
+
"--objective", "-o",
|
|
118
|
+
type=str,
|
|
119
|
+
default=None,
|
|
120
|
+
help="Custom evaluation objective/prompt",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
parser.add_argument(
|
|
124
|
+
"--sensitive", "-s",
|
|
125
|
+
type=str,
|
|
126
|
+
default=None,
|
|
127
|
+
help="Comma-separated list of sensitive columns (auto-detected if not specified)",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Configuration
|
|
131
|
+
parser.add_argument(
|
|
132
|
+
"--config", "-c",
|
|
133
|
+
type=str,
|
|
134
|
+
default=None,
|
|
135
|
+
help="Path to YAML configuration file (uses bundled default if omitted)",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
parser.add_argument(
|
|
139
|
+
"--model", "-m",
|
|
140
|
+
type=str,
|
|
141
|
+
default=None,
|
|
142
|
+
help="Override default model from config",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
parser.add_argument(
|
|
146
|
+
"--env-file", "-e",
|
|
147
|
+
type=str,
|
|
148
|
+
default=None,
|
|
149
|
+
help="Path to .env file for loading API keys",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Output
|
|
153
|
+
parser.add_argument(
|
|
154
|
+
"--output", "-O",
|
|
155
|
+
type=str,
|
|
156
|
+
default=None,
|
|
157
|
+
help="Output directory for reports (default: reports/)",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
parser.add_argument(
|
|
161
|
+
"--no-pdf",
|
|
162
|
+
action="store_true",
|
|
163
|
+
help="Skip PDF generation",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Modes
|
|
167
|
+
parser.add_argument(
|
|
168
|
+
"--verify", "-V",
|
|
169
|
+
action="store_true",
|
|
170
|
+
help="Verify configuration only (don't run evaluation)",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
parser.add_argument(
|
|
174
|
+
"--quiet", "-q",
|
|
175
|
+
action="store_true",
|
|
176
|
+
help="Suppress progress messages",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return parser.parse_args()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def load_env_file(env_path: str) -> None:
|
|
183
|
+
"""Load environment variables from a .env file."""
|
|
184
|
+
env_path = Path(env_path)
|
|
185
|
+
if not env_path.exists():
|
|
186
|
+
print(f"Warning: .env file not found: {env_path}")
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
print(f"Loading environment from: {env_path}")
|
|
190
|
+
|
|
191
|
+
with open(env_path, "r") as f:
|
|
192
|
+
for line in f:
|
|
193
|
+
line = line.strip()
|
|
194
|
+
if not line or line.startswith("#"):
|
|
195
|
+
continue
|
|
196
|
+
if "=" in line:
|
|
197
|
+
key, value = line.split("=", 1)
|
|
198
|
+
key = key.strip()
|
|
199
|
+
value = value.strip().strip('"').strip("'")
|
|
200
|
+
os.environ[key] = value
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
if __name__ == "__main__":
|
|
204
|
+
sys.exit(main())
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import yaml
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, List, Optional, Union
|
|
9
|
+
import shutil
|
|
10
|
+
import traceback
|
|
11
|
+
from equiaudit.models.agents.base_agent import APIError
|
|
12
|
+
|
|
13
|
+
import pandas as pd
|
|
14
|
+
from equiaudit.pipeline.pipeline import DatasetEvaluationPipeline
|
|
15
|
+
from equiaudit.pipeline.utils import TECHNIQUE_DISPLAY
|
|
16
|
+
|
|
17
|
+
# Root of the equiaudit package (parent of cli/)
|
|
18
|
+
_SRC_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class EvaluationResult:
|
|
23
|
+
"""Result of a fairness evaluation run."""
|
|
24
|
+
|
|
25
|
+
success: bool
|
|
26
|
+
dataset: str
|
|
27
|
+
target_column: Optional[str]
|
|
28
|
+
report_dir: str
|
|
29
|
+
pdf_path: Optional[str]
|
|
30
|
+
markdown_path: Optional[str]
|
|
31
|
+
json_path: Optional[str]
|
|
32
|
+
error: Optional[str] = None
|
|
33
|
+
warnings: List[str] = field(default_factory=list)
|
|
34
|
+
stages_completed: List[str] = field(default_factory=list)
|
|
35
|
+
|
|
36
|
+
def __repr__(self) -> str:
|
|
37
|
+
status = "SUCCESS" if self.success else "FAILED"
|
|
38
|
+
return f"EvaluationResult({status}, dataset={self.dataset}, report_dir={self.report_dir})"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class VerificationResult:
|
|
43
|
+
"""Result of configuration verification."""
|
|
44
|
+
|
|
45
|
+
success: bool
|
|
46
|
+
config_path: str
|
|
47
|
+
model_name: str
|
|
48
|
+
errors: List[str] = field(default_factory=list)
|
|
49
|
+
warnings: List[str] = field(default_factory=list)
|
|
50
|
+
|
|
51
|
+
def __repr__(self) -> str:
|
|
52
|
+
status = "OK" if self.success else "FAILED"
|
|
53
|
+
return f"VerificationResult({status}, model={self.model_name})"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class FairnessEvaluator:
|
|
57
|
+
"""
|
|
58
|
+
Main class for running fairness evaluations on datasets.
|
|
59
|
+
|
|
60
|
+
This class wraps the DatasetEvaluationPipeline and provides a simplified
|
|
61
|
+
interface for headless (non-GUI) evaluation runs.
|
|
62
|
+
|
|
63
|
+
Example:
|
|
64
|
+
>>> evaluator = FairnessEvaluator(config_path="models/config.yml")
|
|
65
|
+
>>> result = evaluator.verify()
|
|
66
|
+
>>> if result.success:
|
|
67
|
+
... eval_result = evaluator.evaluate("adult-all.csv", target="Income")
|
|
68
|
+
... print(f"Report saved to: {eval_result.pdf_path}")
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
config_path: Optional[str] = None,
|
|
74
|
+
output_dir: Optional[str] = None,
|
|
75
|
+
model: Optional[str] = None,
|
|
76
|
+
api_key: Optional[str] = None,
|
|
77
|
+
verbose: bool = True,
|
|
78
|
+
):
|
|
79
|
+
"""
|
|
80
|
+
Initialize the FairnessEvaluator.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
config_path: Path to the YAML configuration file. If None, uses default.
|
|
84
|
+
output_dir: Base directory for reports. If None, uses project's reports/ folder.
|
|
85
|
+
model: Override the default model from config. If None, uses config default.
|
|
86
|
+
api_key: API key for the model provider (OpenRouter/Gemini). Overrides env variable.
|
|
87
|
+
verbose: Whether to print progress messages.
|
|
88
|
+
"""
|
|
89
|
+
self.verbose = verbose
|
|
90
|
+
self.model_override = model
|
|
91
|
+
self.api_key = api_key
|
|
92
|
+
|
|
93
|
+
# Resolve config path
|
|
94
|
+
if config_path is None:
|
|
95
|
+
config_path = os.path.join(_SRC_DIR, "models", "config.yml")
|
|
96
|
+
self.config_path = os.path.abspath(config_path)
|
|
97
|
+
|
|
98
|
+
# Resolve output directory
|
|
99
|
+
if output_dir is None:
|
|
100
|
+
# Default to project root's reports/ folder
|
|
101
|
+
project_root = os.path.dirname(_SRC_DIR)
|
|
102
|
+
output_dir = os.path.join(project_root, "reports")
|
|
103
|
+
self.output_dir = os.path.abspath(output_dir)
|
|
104
|
+
|
|
105
|
+
# Pipeline will be initialized lazily
|
|
106
|
+
self._pipeline = None
|
|
107
|
+
self._initialized = False
|
|
108
|
+
|
|
109
|
+
# Verbose initialization happens when evaluation starts
|
|
110
|
+
|
|
111
|
+
def _ensure_initialized(self) -> None:
|
|
112
|
+
"""Lazily initialize the pipeline."""
|
|
113
|
+
if self._initialized:
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
self._pipeline = DatasetEvaluationPipeline(
|
|
117
|
+
config_path=self.config_path,
|
|
118
|
+
default_model=self.model_override,
|
|
119
|
+
api_key=self.api_key,
|
|
120
|
+
)
|
|
121
|
+
self._initialized = True
|
|
122
|
+
|
|
123
|
+
def verify(self) -> VerificationResult:
|
|
124
|
+
"""
|
|
125
|
+
Verify that the configuration is valid and the system is ready.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
VerificationResult with status and any errors/warnings.
|
|
129
|
+
"""
|
|
130
|
+
errors: List[str] = []
|
|
131
|
+
warnings: List[str] = []
|
|
132
|
+
model_name = "unknown"
|
|
133
|
+
|
|
134
|
+
# Check config file exists
|
|
135
|
+
if not os.path.exists(self.config_path):
|
|
136
|
+
errors.append(f"Configuration file not found: {self.config_path}")
|
|
137
|
+
return VerificationResult(
|
|
138
|
+
success=False,
|
|
139
|
+
config_path=self.config_path,
|
|
140
|
+
model_name=model_name,
|
|
141
|
+
errors=errors,
|
|
142
|
+
warnings=warnings,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Try to initialize pipeline
|
|
146
|
+
try:
|
|
147
|
+
self._ensure_initialized()
|
|
148
|
+
model_name = self._pipeline.agent_manager.config.get("default_model", "unknown")
|
|
149
|
+
|
|
150
|
+
if self.verbose:
|
|
151
|
+
print(f"Configuration loaded successfully")
|
|
152
|
+
print(f" Default model: {model_name}")
|
|
153
|
+
except Exception as e:
|
|
154
|
+
errors.append(f"Failed to initialize pipeline: {str(e)}")
|
|
155
|
+
return VerificationResult(
|
|
156
|
+
success=False,
|
|
157
|
+
config_path=self.config_path,
|
|
158
|
+
model_name=model_name,
|
|
159
|
+
errors=errors,
|
|
160
|
+
warnings=warnings,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Check output directory
|
|
164
|
+
try:
|
|
165
|
+
os.makedirs(self.output_dir, exist_ok=True)
|
|
166
|
+
if self.verbose:
|
|
167
|
+
print(f"Output directory ready: {self.output_dir}")
|
|
168
|
+
except Exception as e:
|
|
169
|
+
errors.append(f"Cannot create output directory: {str(e)}")
|
|
170
|
+
|
|
171
|
+
# Check API key if using OpenRouter
|
|
172
|
+
model_config = self._pipeline.agent_manager.config.get("models", {}).get(model_name, {})
|
|
173
|
+
provider = model_config.get("provider", "")
|
|
174
|
+
|
|
175
|
+
if provider == "openrouter":
|
|
176
|
+
api_key = os.environ.get("OPENROUTER_API_KEY")
|
|
177
|
+
if not api_key:
|
|
178
|
+
warnings.append("OPENROUTER_API_KEY environment variable not set")
|
|
179
|
+
elif provider == "gemini":
|
|
180
|
+
api_key = os.environ.get("GOOGLE_API_KEY")
|
|
181
|
+
if not api_key:
|
|
182
|
+
warnings.append("GOOGLE_API_KEY environment variable not set")
|
|
183
|
+
|
|
184
|
+
success = len(errors) == 0
|
|
185
|
+
|
|
186
|
+
if self.verbose:
|
|
187
|
+
if success:
|
|
188
|
+
print("\nVerification: PASSED")
|
|
189
|
+
if warnings:
|
|
190
|
+
print(f" Warnings: {len(warnings)}")
|
|
191
|
+
for w in warnings:
|
|
192
|
+
print(f" - {w}")
|
|
193
|
+
else:
|
|
194
|
+
print("\nVerification: FAILED")
|
|
195
|
+
for e in errors:
|
|
196
|
+
print(f" ERROR: {e}")
|
|
197
|
+
|
|
198
|
+
return VerificationResult(
|
|
199
|
+
success=success,
|
|
200
|
+
config_path=self.config_path,
|
|
201
|
+
model_name=model_name,
|
|
202
|
+
errors=errors,
|
|
203
|
+
warnings=warnings,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def evaluate(
|
|
207
|
+
self,
|
|
208
|
+
data: Union[str, Path, pd.DataFrame],
|
|
209
|
+
target: Optional[str] = None,
|
|
210
|
+
objective: Optional[str] = None,
|
|
211
|
+
sensitive_columns: Optional[List[str]] = None,
|
|
212
|
+
sensitive_pairs: Optional[List[List[str]]] = None,
|
|
213
|
+
mitigation_techniques: Optional[List[str]] = None,
|
|
214
|
+
ml_config: Optional[Dict[str, Any]] = None,
|
|
215
|
+
output_dir: Optional[str] = None,
|
|
216
|
+
generate_pdf: bool = True,
|
|
217
|
+
max_pairs: Optional[int] = None,
|
|
218
|
+
) -> EvaluationResult:
|
|
219
|
+
"""
|
|
220
|
+
Run a fairness evaluation on a dataset.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
data: Path to CSV file, or a pandas DataFrame.
|
|
224
|
+
target: Target column name for classification. Auto-detected if None.
|
|
225
|
+
objective: Custom objective/prompt for the evaluation.
|
|
226
|
+
sensitive_columns: List of sensitive attribute columns to analyze.
|
|
227
|
+
If None, uses config settings (auto-detect or restricted list).
|
|
228
|
+
sensitive_pairs: List of attribute pairs to analyze for intersectionality.
|
|
229
|
+
Each pair is a list of two column names, e.g. [["Sex", "Race"]].
|
|
230
|
+
If None, uses config settings (auto-select or restricted list).
|
|
231
|
+
mitigation_techniques: List of bias mitigation techniques to apply.
|
|
232
|
+
Options: "reweighting", "smote", "resampling",
|
|
233
|
+
"oversampling", "undersampling".
|
|
234
|
+
If None, reads from config. If config also has none,
|
|
235
|
+
mitigation stage is skipped entirely.
|
|
236
|
+
ml_config: ML model configuration for fairness metrics.
|
|
237
|
+
output_dir: Override output directory for this run.
|
|
238
|
+
generate_pdf: Whether to generate PDF report.
|
|
239
|
+
max_pairs: Maximum number of sensitive attribute pairs to analyze.
|
|
240
|
+
Only used when sensitive_pairs is None and config type is "auto".
|
|
241
|
+
If None, uses value from config.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
EvaluationResult with paths to generated reports.
|
|
245
|
+
"""
|
|
246
|
+
self._ensure_initialized()
|
|
247
|
+
|
|
248
|
+
warnings: List[str] = []
|
|
249
|
+
stages_completed: List[str] = []
|
|
250
|
+
|
|
251
|
+
# Resolve data input
|
|
252
|
+
if isinstance(data, pd.DataFrame):
|
|
253
|
+
# Save DataFrame to temporary CSV
|
|
254
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
255
|
+
temp_csv_name = f"temp_dataset_{timestamp}.csv"
|
|
256
|
+
data_dir = os.path.join(_SRC_DIR, "data")
|
|
257
|
+
os.makedirs(data_dir, exist_ok=True)
|
|
258
|
+
temp_csv_path = os.path.join(data_dir, temp_csv_name)
|
|
259
|
+
data.to_csv(temp_csv_path, index=False)
|
|
260
|
+
dataset_name = temp_csv_name
|
|
261
|
+
if self.verbose:
|
|
262
|
+
print(f"DataFrame saved to: {temp_csv_path}")
|
|
263
|
+
else:
|
|
264
|
+
data_path = Path(data)
|
|
265
|
+
if not data_path.exists():
|
|
266
|
+
# Try looking in the bundled data directory
|
|
267
|
+
data_path = Path(_SRC_DIR) / "data" / data_path.name
|
|
268
|
+
|
|
269
|
+
if not data_path.exists():
|
|
270
|
+
return EvaluationResult(
|
|
271
|
+
success=False,
|
|
272
|
+
dataset=str(data),
|
|
273
|
+
target_column=target,
|
|
274
|
+
report_dir="",
|
|
275
|
+
pdf_path=None,
|
|
276
|
+
markdown_path=None,
|
|
277
|
+
json_path=None,
|
|
278
|
+
error=f"Dataset not found: {data}",
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
dataset_name = data_path.name
|
|
282
|
+
|
|
283
|
+
# Copy to data/ if not already there
|
|
284
|
+
expected_path = Path(_SRC_DIR) / "data" / dataset_name
|
|
285
|
+
if data_path.resolve() != expected_path.resolve():
|
|
286
|
+
os.makedirs(expected_path.parent, exist_ok=True)
|
|
287
|
+
shutil.copy2(data_path, expected_path)
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
# Load evaluation config directly from YAML file
|
|
291
|
+
cfg: Dict[str, Any] = {}
|
|
292
|
+
if os.path.exists(self.config_path):
|
|
293
|
+
with open(self.config_path, "r", encoding="utf-8") as _f:
|
|
294
|
+
cfg = yaml.safe_load(_f) or {}
|
|
295
|
+
|
|
296
|
+
# Fall back to config target_column if not explicitly provided
|
|
297
|
+
if target is None:
|
|
298
|
+
target = cfg.get("target_column") or None
|
|
299
|
+
if self.verbose and target:
|
|
300
|
+
print(f"Using target column from config: {target}")
|
|
301
|
+
|
|
302
|
+
# Build objective prompt AFTER target fallback
|
|
303
|
+
if objective is None:
|
|
304
|
+
target_str = f"Target: {target}. " if target else ""
|
|
305
|
+
objective = (
|
|
306
|
+
f"Evaluate the dataset '{dataset_name}' for data quality and fairness issues. "
|
|
307
|
+
f"{target_str}Provide a detailed report highlighting any problems found and suggestions for improvement."
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
if self.verbose:
|
|
311
|
+
print(f"\nEvaluating '{dataset_name}' (target: {target or 'auto-detect'})")
|
|
312
|
+
|
|
313
|
+
# Handle sensitive attribute analysis configuration
|
|
314
|
+
sens_attr_config = cfg.get("sensitive_attribute_analysis", {})
|
|
315
|
+
if sensitive_columns is None and sens_attr_config.get("type") == "restricted":
|
|
316
|
+
# Use restricted list from config
|
|
317
|
+
sensitive_columns = sens_attr_config.get("attributes", [])
|
|
318
|
+
if self.verbose and sensitive_columns:
|
|
319
|
+
print(f"Using restricted sensitive attributes: {sensitive_columns}")
|
|
320
|
+
|
|
321
|
+
# Handle pair evaluation configuration
|
|
322
|
+
pair_config = cfg.get("pair_evaluation", {})
|
|
323
|
+
final_sensitive_pairs = sensitive_pairs # Start with explicit parameter
|
|
324
|
+
|
|
325
|
+
if final_sensitive_pairs is None and pair_config.get("type") == "restricted":
|
|
326
|
+
# Use restricted pairs from config
|
|
327
|
+
final_sensitive_pairs = pair_config.get("pairs", [])
|
|
328
|
+
if self.verbose and final_sensitive_pairs:
|
|
329
|
+
print(f"Using restricted pairs: {final_sensitive_pairs}")
|
|
330
|
+
|
|
331
|
+
# Load max_pairs from config if not provided
|
|
332
|
+
if max_pairs is None:
|
|
333
|
+
max_pairs = pair_config.get("max_pairs")
|
|
334
|
+
|
|
335
|
+
# Handle mitigation techniques configuration
|
|
336
|
+
final_mitigation_config = None
|
|
337
|
+
techniques = mitigation_techniques
|
|
338
|
+
if techniques is None:
|
|
339
|
+
mitigation_cfg = cfg.get("mitigation_techniques", {})
|
|
340
|
+
techniques = mitigation_cfg.get("techniques", [])
|
|
341
|
+
if techniques:
|
|
342
|
+
methods = {}
|
|
343
|
+
for t in techniques:
|
|
344
|
+
display = TECHNIQUE_DISPLAY.get(t.lower())
|
|
345
|
+
if display:
|
|
346
|
+
methods[display] = {}
|
|
347
|
+
else:
|
|
348
|
+
warnings.append(f"Unknown mitigation technique '{t}', skipped.")
|
|
349
|
+
if methods:
|
|
350
|
+
final_mitigation_config = {"methods": methods}
|
|
351
|
+
if self.verbose:
|
|
352
|
+
print(f"Applying mitigation: {list(methods.keys())}")
|
|
353
|
+
|
|
354
|
+
final_ml_config = ml_config
|
|
355
|
+
if final_ml_config is None:
|
|
356
|
+
ml_eval_cfg = cfg.get("ml_evaluation", {})
|
|
357
|
+
if ml_eval_cfg:
|
|
358
|
+
m_type = ml_eval_cfg.get("model_type", "Random Forest")
|
|
359
|
+
m_params = ml_eval_cfg.get("model_params", {}).get(m_type, {})
|
|
360
|
+
final_ml_config = {
|
|
361
|
+
"enabled": True,
|
|
362
|
+
"model_type": m_type,
|
|
363
|
+
"model_params": m_params,
|
|
364
|
+
"test_size": ml_eval_cfg.get("test_size", 0.25)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
# Build discretization config from YAML
|
|
368
|
+
disc_setting = sens_attr_config.get("discretization", "enable")
|
|
369
|
+
disc_enabled = disc_setting in ("enable", True, "true", "yes")
|
|
370
|
+
disc_method = sens_attr_config.get("method", "auto")
|
|
371
|
+
disc_bins = int(sens_attr_config.get("number_of_bins", 5))
|
|
372
|
+
disc_threshold = int(sens_attr_config.get("continuous_threshold", 10))
|
|
373
|
+
discretization_config = {
|
|
374
|
+
"discretization_enabled": disc_enabled,
|
|
375
|
+
"discretization_method": disc_method,
|
|
376
|
+
"discretization_bins": disc_bins,
|
|
377
|
+
"discretization_threshold": disc_threshold,
|
|
378
|
+
}
|
|
379
|
+
if self.verbose and disc_enabled:
|
|
380
|
+
print(f"Discretization: method={disc_method}, bins={disc_bins}, threshold={disc_threshold}")
|
|
381
|
+
|
|
382
|
+
# Run the pipeline
|
|
383
|
+
self._pipeline.evaluate_dataset(
|
|
384
|
+
user_prompt=objective,
|
|
385
|
+
confirmed_sensitive=sensitive_columns,
|
|
386
|
+
sensitive_pairs=final_sensitive_pairs,
|
|
387
|
+
ml_config=final_ml_config,
|
|
388
|
+
max_pairs=max_pairs,
|
|
389
|
+
mitigation_config=final_mitigation_config,
|
|
390
|
+
discretization_config=discretization_config,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
# Generate report
|
|
394
|
+
self._pipeline.generate_report()
|
|
395
|
+
|
|
396
|
+
report_dir = self._pipeline.report_dir
|
|
397
|
+
md_path = os.path.join(report_dir, "evaluation_report.md")
|
|
398
|
+
json_path = os.path.join(report_dir, "stage_data.json")
|
|
399
|
+
|
|
400
|
+
# Determine PDF path
|
|
401
|
+
pdf_path = None
|
|
402
|
+
if generate_pdf:
|
|
403
|
+
pdf_path = os.path.join(report_dir, "evaluation_report.pdf")
|
|
404
|
+
if not os.path.exists(pdf_path):
|
|
405
|
+
pdf_path = None
|
|
406
|
+
warnings.append("PDF generation may have failed")
|
|
407
|
+
|
|
408
|
+
# Get completed stages
|
|
409
|
+
stages_completed = list(self._pipeline.evaluation_results.get("stages", {}).keys())
|
|
410
|
+
|
|
411
|
+
if self.verbose:
|
|
412
|
+
print(f"\nEvaluation completed successfully!")
|
|
413
|
+
print(f" Report directory: {report_dir}")
|
|
414
|
+
print(f" Stages completed: {len(stages_completed)}")
|
|
415
|
+
if pdf_path:
|
|
416
|
+
print(f" PDF report: {pdf_path}")
|
|
417
|
+
|
|
418
|
+
return EvaluationResult(
|
|
419
|
+
success=True,
|
|
420
|
+
dataset=dataset_name,
|
|
421
|
+
target_column=target or self._pipeline.target_column,
|
|
422
|
+
report_dir=report_dir,
|
|
423
|
+
pdf_path=pdf_path,
|
|
424
|
+
markdown_path=md_path,
|
|
425
|
+
json_path=json_path,
|
|
426
|
+
warnings=warnings,
|
|
427
|
+
stages_completed=stages_completed,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
except Exception as e:
|
|
431
|
+
|
|
432
|
+
# Handle API errors with clean messages
|
|
433
|
+
if isinstance(e, APIError):
|
|
434
|
+
error_msg = str(e)
|
|
435
|
+
if self.verbose:
|
|
436
|
+
print(f"\nError: {error_msg}")
|
|
437
|
+
else:
|
|
438
|
+
error_msg = f"{type(e).__name__}: {str(e)}"
|
|
439
|
+
if self.verbose:
|
|
440
|
+
print(f"\nEvaluation failed: {error_msg}")
|
|
441
|
+
traceback.print_exc()
|
|
442
|
+
|
|
443
|
+
return EvaluationResult(
|
|
444
|
+
success=False,
|
|
445
|
+
dataset=dataset_name,
|
|
446
|
+
target_column=target,
|
|
447
|
+
report_dir=getattr(self._pipeline, "report_dir", ""),
|
|
448
|
+
pdf_path=None,
|
|
449
|
+
markdown_path=None,
|
|
450
|
+
json_path=None,
|
|
451
|
+
error=error_msg,
|
|
452
|
+
warnings=warnings,
|
|
453
|
+
stages_completed=stages_completed,
|
|
454
|
+
)
|
|
455
|
+
|