Uncover the bias hiding in your data.
Upload a dataset or ML model. Unveil finds unfair outcomes across sensitive attributes, explains what's driving them, and produces a plain-English compliance report - in under a minute.
Built for the Google AI Solution Challenge using Gemini 2.5 Flash.
| 🌐 Frontend | https://unveil-201cc.web.app/ |
| 📡 Backend API | https://unveil-899475904423.europe-west1.run.app/docs |
Unveil is a full-stack web app that audits datasets and machine learning models for algorithmic bias. It's aimed at developers, data scientists, and compliance teams who need to know whether their data or model is treating different demographic groups unfairly - without needing a PhD in fairness research to interpret the results.
You upload a CSV (or XLSX, JSON). Unveil does three things:
- Finds which columns are sensitive attributes - age, sex, race, marital status, etc. - using Gemini to classify them, with a rules-based fallback for common column names.
- Measures fairness across those attributes using disparate impact ratios (the legal 80% / four-fifths rule), approval gaps, and per-group breakdown statistics.
- Catches proxy columns - seemingly neutral columns like
relationship,occupation, orzip_codethat correlate so strongly with a sensitive attribute that removing the obvious column doesn't fix anything.
On top of that, if you upload a .pkl model alongside the dataset, Unveil runs black-box counterfactual probing (flipping one attribute at a time and watching the prediction change) plus SHAP feature attribution to show which inputs are actually steering decisions.
The whole audit ends with a Gemini-generated compliance narrative - an executive summary, per-finding breakdown, proxy risk explanation, and actionable recommendations - written for a non-technical reader, ready to hand to legal or product.
Algorithmic bias in real deployments isn't usually obvious. A hiring model doesn't say "don't hire women" - it says "candidates from relationship status = Husband score higher." A lending model doesn't say "deny minorities" - it says "zip code 94103 is high-risk." The sensitive attribute is already gone from the data; its proxy is doing the work instead.
Unveil was built specifically to surface that second layer. It's not enough to remove sex and race from your dataset. You need to know that relationship has a Cramér's V of 0.73 with sex, or that occupation has a mutual information score of 0.41 with race. That's what the proxy detector does.
| Layer | What |
|---|---|
| Frontend | React 19, Vite, Tailwind CSS v4, Framer Motion, Recharts |
| Backend | Python, FastAPI, Uvicorn |
| AI | Gemini 2.5 Flash (column classification + report generation) |
| Auth | Firebase Auth (optional) / localStorage fallback |
| Storage | Firestore (optional) / localStorage fallback |
| ML | scikit-learn, SHAP, SciPy |
| File formats | CSV, XLSX, JSON, JSONL, TSV |
unveil/
│
├── src/ # React frontend
│ ├── pages/
│ │ ├── Landing.jsx # Homepage / marketing
│ │ ├── Upload.jsx # File upload + analysis orchestration
│ │ ├── DatasetAudit.jsx # Per-column bias results
│ │ ├── ModelAudit.jsx # Model fairness + SHAP chart
│ │ ├── Report.jsx # Gemini compliance narrative
│ │ ├── Dashboard.jsx # Saved audit history
│ │ ├── Glossary.jsx # Plain-English term definitions
│ │ ├── Login.jsx
│ │ └── SignUp.jsx
│ │
│ ├── components/
│ │ ├── Navbar.jsx
│ │ ├── ColumnCard.jsx # Per-column bias breakdown card
│ │ ├── BiasGauge.jsx # Visual fairness ratio indicator
│ │ ├── SliceChart.jsx # Per-group approval rate bar chart
│ │ ├── ShapChart.jsx # SHAP feature importance chart
│ │ ├── ProxyAlert.jsx # Proxy column warning banner
│ │ └── GeminiReport.jsx # Report renderer
│ │
│ └── lib/
│ ├── api.js # Frontend → backend HTTP client
│ ├── gemini.js # Direct Gemini API fallback (browser-side)
│ ├── auth.js # Firebase Auth + local account mode
│ ├── storage.js # Firestore + localStorage audit persistence
│ ├── AuditContext.jsx # Global audit state (React context)
│ └── fileParser.js # Client-side CSV/XLSX preview
│
├── backend/
│ ├── api.py # FastAPI routes (HTTP only - no business logic)
│ ├── pipeline.py # Analysis orchestration (Part A + Part B)
│ ├── ingestor.py # File ingestion → normalized DataFrame
│ ├── gemini_classifier.py # Column classification via Gemini + rules fallback
│ ├── proxy_detection.py # Cramér's V + mutual information proxy detection
│ ├── stats.py # Disparate impact, p-values, per-group stats
│ ├── slice_eval.py # Per-group approval rate / FPR / FNR calculation
│ ├── counterfactual_engine.py # Row cloning + attribute flipping for model probing
│ ├── probe_generator.py # Black-box model probing (100+ probes / attribute)
│ ├── shap_explainer.py # TreeExplainer / KernelExplainer wrapper
│ ├── train_demo_model.py # Script to (re)train the bundled demo model
│ └── demo_model.pkl # Pre-trained sklearn model on UCI Adult
│
├── data/
│ ├── adult.csv # UCI Adult Income dataset (raw)
│ └── adult_fixed.csv # UCI Adult Income dataset (cleaned)
│
├── models/
│ └── adult_demo_model.pkl # Pre-trained demo model for sample audit flow
│
├── setup/
│ ├── start.sh # macOS / Linux startup script
│ ├── start.bat # Windows CMD startup script
│ └── start.ps1 # Windows PowerShell startup script
│
├── docs/requirements.txt # Python dependencies
├── package.json # Node dependencies
└── docs/ # Documentation
ingestor.py → gemini_classifier.py → proxy_detection.py → stats.py
1. Ingest - reads CSV/XLSX/JSON into a pandas DataFrame, normalises column names, extracts column metadata (dtype, unique count, sample values).
2. Classify - Gemini 2.5 Flash reads the column names and sample values and assigns each one a role: PROTECTED (sensitive attribute), OUTCOME (prediction target), AMBIGUOUS (proxy candidate), or NEUTRAL. A rules-based pass runs first and handles obvious names like sex, race, age, income without burning API quota.
3. Proxy detection - for every NEUTRAL or AMBIGUOUS column, compute:
- Cramér's V against each
PROTECTEDcolumn (association strength, 0-1) - Mutual information between them
Thresholds: Cramér's V ≥ 0.30 AND MI ≥ 0.10 → PROXY. Either alone → WEAK_PROXY.
4. Bias stats - for each PROTECTED column, slice the dataset by group and compute:
- Disparate impact ratio:
min(group approval rate) / max(group approval rate). Below 0.80 fails the EEOC four-fifths rule. - Parity gap: raw percentage-point difference between best and worst group. Above 10pp is flagged.
- p-value (chi-squared test): below 0.05 = statistically significant.
- Verdict:
BIASED,AMBIGUOUS, orCLEAN.
probe_generator.py → counterfactual_engine.py → shap_explainer.py
Counterfactual probing - generates 100+ synthetic row pairs per protected attribute. Each pair is identical except one attribute is flipped (e.g. sex: Male → Female). The model scores both. A large mean shift + low p-value = the model is using that attribute.
SHAP - runs TreeExplainer for sklearn tree-based models (fast, exact) or KernelExplainer for everything else (model-agnostic, slower). Returns per-feature importance ranked by mean absolute SHAP value. Protected and proxy columns are highlighted in the chart.
Four Gemini calls in sequence, each with its own large token budget:
- Executive Summary (1024 tokens)
- Critical Findings (8192 tokens - one paragraph per biased column)
- Proxy Risk (2048 tokens)
- Recommendations (1536 tokens)
Sections are generated independently. If one hits a rate limit, the others still complete. Results are cached by content hash so regenerating the same audit is free.
- Node.js 18+
- Python 3.10+
- A Gemini API key (free at aistudio.google.com)
git clone https://github.com/Jay-Jay-Tee/unveil.git
cd unveil# Frontend
npm install
# Backend
pip install -r docs/requirements.txtcp .env.example .envThen edit .env with at least your Gemini key:
# Required
VITE_GEMINI_API_KEY=your_gemini_api_key_here
GEMINI_API_KEY=your_gemini_api_key_here
# Backend URL - leave as-is for local development
VITE_API_URL=http://localhost:8001/api
# Optional: disable auth requirement for local testing only
# VITE_REQUIRE_AUTH_FOR_ANALYSIS=false
# Optional: Firebase (for cloud accounts + audit sync across devices)
# Without these, the app uses localStorage - fully functional but device-local
# VITE_FIREBASE_API_KEY=
# VITE_FIREBASE_AUTH_DOMAIN=
# VITE_FIREBASE_PROJECT_ID=
# VITE_FIREBASE_STORAGE_BUCKET=
# VITE_FIREBASE_MESSAGING_SENDER_ID=
# VITE_FIREBASE_APP_ID=macOS / Linux:
chmod +x setup/start.sh
./setup/start.shWindows (CMD):
setup\start.batWindows (PowerShell):
.\setup\start.ps1The scripts check for dependencies, install anything missing, and start both services:
- Frontend → http://localhost:5173
- Backend API → http://localhost:8001
Manual startup (any OS):
# Terminal 1
npm run frontend
# Terminal 2
npm run backend- Create an account (or continue as guest - audits won't be saved in guest mode)
- Upload a dataset - drag a CSV, XLSX, or JSON file onto the upload zone.
data/adult.csvin the repo is a ready-to-use example. - Optionally upload a model - a
.pklsklearn model. If you skip this, Unveil audits the dataset only. - Hit Run audit - the status panel shows each step as it completes.
- View results on the Dataset Audit page - columns are sorted by severity, each with its fairness ratio, approval gap, per-group chart, and proxy strength.
- If you uploaded a model, the Model Audit page shows counterfactual probing results and the SHAP feature chart.
- Hit Generate report to get the Gemini compliance narrative.
- Audits are auto-saved to your dashboard on completion.
Without Firebase (default):
Accounts are stored in localStorage. Each email address creates a stable local account - signing up with you@example.com always returns the same account on the same browser. Audits are saved locally under your account's uid. Nothing leaves your device.
With Firebase (set the VITE_FIREBASE_* env vars):
Full cloud accounts via Firebase Auth. Audits persist in Firestore and sync across devices. This is the production mode.
Guest mode lets you run audits without signing up. The results are shown in full but not saved to a dashboard. The "My audits" page shows a sign-up prompt.
| Term | What it means |
|---|---|
| Disparate impact ratio | min_group_rate / max_group_rate. Below 0.80 fails the EEOC four-fifths rule. |
| Parity gap | Raw percentage-point difference between best and worst group. Above 10pp is flagged. |
| p-value | Probability the gap is random noise. Below 0.05 = statistically significant. |
| Proxy column | A neutral-looking column that encodes a sensitive attribute. Cramér's V ≥ 0.30 = proxy. |
| Cramér's V | Association strength between two categorical columns, 0-1. |
| Counterfactual probe | Flip one attribute, re-score, measure the change. If the model cares, prediction shifts. |
| SHAP value | How much credit each feature gets for each individual prediction. |
The full glossary is in the app at /glossary.
# Frontend (Vitest)
npm test
# Backend (pytest)
python -m pytest tests/| Variable | Required | Description |
|---|---|---|
VITE_GEMINI_API_KEY |
Yes | Gemini API key - get one free at aistudio.google.com |
GEMINI_API_KEY |
Yes | Gemini API key for the backend |
VITE_API_URL |
No | Backend base URL. Default: /api (proxied by Vite) |
VITE_REQUIRE_AUTH_FOR_ANALYSIS |
No | Set false to skip auth checks locally |
VITE_USE_MOCK |
No | Set false to disable mock data fallback when backend is offline |
AUTH_REQUIRED |
No | Set false on backend to skip Firebase token checks locally |
ALLOW_LOCALHOST_CORS |
No | Set false in production to disallow localhost origins |
FIREBASE_SERVICE_ACCOUNT_PATH |
No | Absolute path to Firebase service account JSON |
VITE_FIREBASE_API_KEY |
No | Firebase project API key |
VITE_FIREBASE_AUTH_DOMAIN |
No | e.g. your-project.firebaseapp.com |
VITE_FIREBASE_PROJECT_ID |
No | Your Firebase project ID |
VITE_FIREBASE_STORAGE_BUCKET |
No | e.g. your-project.appspot.com |
VITE_FIREBASE_MESSAGING_SENDER_ID |
No | Firebase sender ID |
VITE_FIREBASE_APP_ID |
No | Firebase app ID |