November 20, 2025
The Indian Fairness Challenge: Bias Detection Beyond Western Frameworks
A multinational bank deployed an AI-powered loan approval system in India. They’d run it through a standard fairness audit - checked for gender bias, age discrimination, and income-based disparities. Everything looked fine.
Six months later, they discovered the system was rejecting qualified applicants at 3x the rate in certain states. A deeper analysis revealed the model had learned regional proxies that correlated with religious and caste demographics.
The standard fairness toolkit had missed it entirely.
Why Western Fairness Frameworks Fail in India
The global AI fairness literature focuses on dimensions that matter in Western contexts:
- Race (particularly Black/White in US)
- Gender (binary male/female)
- Age
- Disability status
These matter in India too. But they’re not the complete picture - or even the most important dimensions for many applications.
India has its own fairness challenges:
Caste
The most significant dimension that Western frameworks ignore entirely.
Caste isn’t a direct input to most models. But proxy variables abound:
- Geographic location (locality, pin code)
- Surname (in many Indian languages, caste is embedded in family names)
- Education institution (some institutions have historical caste associations)
- Occupation (traditional caste-occupation links persist)
- Language patterns (register and dialect can signal caste)
A model trained on historical data learns these patterns. A fairness tool that doesn’t look for caste-based disparities won’t catch it.
Religion
Religious identity correlates with:
- Names (first name patterns differ across communities)
- Geographic concentration
- Dietary preferences (in food delivery or hospitality AI)
- Festival and holiday patterns (in scheduling or demand prediction AI)
Region
India’s regional diversity creates another fairness dimension:
- Language background
- Economic development levels
- Infrastructure quality
- Cultural practices
A model that works well in metro cities might systematically underperform for rural users or users from economically weaker states.
Economic Status
While income is a protected characteristic globally, India’s economic stratification has specific patterns:
- Formal vs. informal sector employment
- Agricultural vs. non-agricultural income
- Joint family vs. nuclear family economics
- Remittance dependency
Intersectionality
These dimensions intersect in India-specific ways:
- Dalit women face compounded discrimination
- Religious minorities in certain regions face specific challenges
- Urban poor vs. rural middle class have different access patterns
Standard fairness metrics that look at one dimension at a time miss these intersections.
A Framework for Indian Fairness
We’ve developed a fairness evaluation framework specifically for Indian AI deployments:
flowchart TB
subgraph PD["1. Protected Dimensions"]
direction LR
A[Gender] ~~~ B[Caste] ~~~ C[Religion]
D[Region] ~~~ E[Economic Status] ~~~ F[Language]
end
subgraph PR["2. Proxy Detection"]
direction LR
G[Pin Code] ~~~ H[Name Patterns] ~~~ I[Institution] ~~~ J[Occupation]
end
subgraph FM["3. Fairness Metrics"]
direction LR
K[Demographic Parity] ~~~ L[Equal Opportunity] ~~~ M[Calibration] ~~~ N[Individual Fairness]
end
subgraph IA["4. Intersectional Analysis"]
direction LR
O[Gender x Caste] ~~~ P[Religion x Region] ~~~ Q[Caste x Economic] ~~~ R[Multi-way]
end
PD --> PR --> FM --> IA
Step 1: Infer Protected Attributes
Since caste and religion aren’t directly collected (for good reason - it would be illegal in many contexts), we need to infer group membership for auditing purposes.
class IndianFairnessAuditor:
def __init__(self):
self.name_classifier = IndianNameClassifier()
self.geo_analyzer = IndianGeographyAnalyzer()
self.institution_mapper = InstitutionDemographicMapper()
def infer_demographics(self, record: dict) -> DemographicInference:
"""
Infer likely demographic group membership for fairness auditing.
Note: This is for aggregate analysis, not individual treatment.
"""
inference = DemographicInference()
# Name-based inference (probabilistic)
if 'name' in record:
name_inference = self.name_classifier.analyze(record['name'])
inference.likely_religion = name_inference.religion
inference.likely_caste_category = name_inference.caste_category
inference.name_confidence = name_inference.confidence
# Geography-based inference
if 'pin_code' in record:
geo_inference = self.geo_analyzer.analyze(record['pin_code'])
inference.region = geo_inference.region
inference.urban_rural = geo_inference.urban_rural
inference.geo_caste_distribution = geo_inference.caste_distribution
inference.geo_religion_distribution = geo_inference.religion_distribution
# Institution-based inference
if 'education_institution' in record:
inst_inference = self.institution_mapper.analyze(record['education_institution'])
inference.institution_demographic_profile = inst_inference
return inference
Important: This inference is for aggregate auditing only. You should never use inferred demographics for individual treatment decisions.
Step 2: Compute Group-Level Metrics
For each protected dimension, compute standard fairness metrics:
def compute_fairness_metrics(
predictions: np.array,
outcomes: np.array,
group_labels: np.array,
positive_label: int = 1
) -> dict:
"""
Compute fairness metrics across groups
"""
groups = np.unique(group_labels)
metrics = {}
# Demographic Parity: P(Y_hat = 1 | Group)
positive_rates = {}
for group in groups:
mask = group_labels == group
positive_rates[group] = (predictions[mask] == positive_label).mean()
metrics['demographic_parity'] = {
'rates': positive_rates,
'disparity_ratio': min(positive_rates.values()) / max(positive_rates.values())
}
# Equal Opportunity: P(Y_hat = 1 | Y = 1, Group)
true_positive_rates = {}
for group in groups:
mask = (group_labels == group) & (outcomes == positive_label)
if mask.sum() > 0:
true_positive_rates[group] = (predictions[mask] == positive_label).mean()
metrics['equal_opportunity'] = {
'rates': true_positive_rates,
'disparity_ratio': min(true_positive_rates.values()) / max(true_positive_rates.values())
}
# Calibration: P(Y = 1 | Y_hat = 1, Group)
precision_by_group = {}
for group in groups:
mask = (group_labels == group) & (predictions == positive_label)
if mask.sum() > 0:
precision_by_group[group] = (outcomes[mask] == positive_label).mean()
metrics['calibration'] = {
'rates': precision_by_group,
'disparity_ratio': min(precision_by_group.values()) / max(precision_by_group.values())
}
return metrics
Step 3: Intersectional Analysis
Look at combinations of protected attributes:
def intersectional_analysis(
predictions: np.array,
outcomes: np.array,
demographic_df: pd.DataFrame,
dimensions: list[str]
) -> pd.DataFrame:
"""
Analyze fairness at intersections of multiple dimensions
"""
results = []
# Generate all intersections
for r in range(1, len(dimensions) + 1):
for combo in combinations(dimensions, r):
combo_name = ' x '.join(combo)
# Group by intersection
grouped = demographic_df.groupby(list(combo))
for group_values, group_idx in grouped.groups.items():
if len(group_idx) < 30: # Skip small groups
continue
group_mask = demographic_df.index.isin(group_idx)
positive_rate = (predictions[group_mask] == 1).mean()
actual_positive_rate = (outcomes[group_mask] == 1).mean()
if actual_positive_rate > 0:
tpr = ((predictions[group_mask] == 1) & (outcomes[group_mask] == 1)).sum() / (outcomes[group_mask] == 1).sum()
else:
tpr = None
results.append({
'intersection': combo_name,
'group': str(group_values),
'n': len(group_idx),
'positive_rate': positive_rate,
'actual_positive_rate': actual_positive_rate,
'true_positive_rate': tpr
})
return pd.DataFrame(results)
Step 4: Proxy Variable Detection
Identify features that act as proxies for protected attributes:
class ProxyDetector:
def detect_proxies(
self,
features: pd.DataFrame,
inferred_demographics: pd.DataFrame
) -> list[ProxyAlert]:
"""
Detect features that correlate strongly with protected attributes
"""
alerts = []
for feature in features.columns:
for protected_attr in inferred_demographics.columns:
if features[feature].dtype in ['object', 'category']:
# Categorical: use chi-squared test
contingency = pd.crosstab(features[feature], inferred_demographics[protected_attr])
chi2, p_value, _, _ = chi2_contingency(contingency)
if p_value < 0.001: # Strong association
alerts.append(ProxyAlert(
feature=feature,
protected_attribute=protected_attr,
test='chi2',
statistic=chi2,
p_value=p_value
))
else:
# Numerical: use correlation
for category in inferred_demographics[protected_attr].unique():
mask = inferred_demographics[protected_attr] == category
correlation = features[feature].corr(mask.astype(float))
if abs(correlation) > 0.3: # Meaningful correlation
alerts.append(ProxyAlert(
feature=feature,
protected_attribute=f"{protected_attr}={category}",
test='correlation',
statistic=correlation,
p_value=None
))
return alerts
Reporting for Compliance
Indian regulators increasingly expect fairness documentation. Here’s what we include in fairness audit reports:
## AI Fairness Audit Report
### Model Information
- Model name: Loan Approval Model v2.3
- Deployment date: 2025-09-15
- Audit date: 2025-11-01
- Audit scope: All decisions in Q3 2025 (n=234,567)
### Protected Dimensions Analyzed
- Gender (direct)
- Caste category (inferred from name + geography)
- Religion (inferred from name)
- Region (from address)
- Urban/Rural (from pin code)
- Economic tier (from income + occupation)
### Fairness Metrics Summary
| Dimension | Demographic Parity Ratio | Equal Opportunity Ratio | Calibration Ratio |
|-----------|-------------------------|------------------------|-------------------|
| Gender | 0.94 | 0.91 | 0.97 |
| Caste Category | 0.82 | 0.78 | 0.89 |
| Religion | 0.88 | 0.85 | 0.92 |
| Region | 0.76 | 0.72 | 0.84 |
| Urban/Rural | 0.71 | 0.68 | 0.81 |
### Intersectional Analysis Findings
- SC/ST women in rural areas: 0.63 equal opportunity ratio (flagged)
- Muslim applicants in specific regions: 0.71 demographic parity (flagged)
- Northeast region overall: 0.68 calibration ratio (flagged)
### Proxy Variables Detected
- Pin code: Strong proxy for caste and religion
- Surname: Strong proxy for caste
- Bank branch: Moderate proxy for economic tier
### Recommendations
1. Remove pin code as direct feature; use only for geographic clustering
2. Implement name anonymization in model input
3. Add regional calibration layer
4. Conduct quarterly re-audit with focus on flagged intersections
Mitigation Strategies
When fairness issues are detected, mitigation options include:
Option 1: Pre-processing
Remove or transform problematic features:
def preprocess_for_fairness(df: pd.DataFrame, proxy_alerts: list[ProxyAlert]) -> pd.DataFrame:
"""
Transform features to reduce proxy effects
"""
df_processed = df.copy()
for alert in proxy_alerts:
if alert.feature in df_processed.columns:
if df_processed[alert.feature].dtype in ['object', 'category']:
# Categorical proxy: group into larger buckets
df_processed[alert.feature] = generalize_categorical(
df_processed[alert.feature],
min_bucket_size=1000
)
else:
# Numerical proxy: add noise or bin
df_processed[alert.feature] = add_laplace_noise(
df_processed[alert.feature],
epsilon=1.0
)
return df_processed
Option 2: In-processing
Add fairness constraints during training:
from fairlearn.reductions import ExponentiatedGradient, DemographicParity
def train_with_fairness_constraint(X, y, sensitive_features):
"""
Train model with demographic parity constraint
"""
base_model = LogisticRegression()
constraint = DemographicParity()
fair_model = ExponentiatedGradient(
base_model,
constraint,
eps=0.05 # Allow 5% disparity
)
fair_model.fit(X, y, sensitive_features=sensitive_features)
return fair_model
Option 3: Post-processing
Adjust predictions to improve fairness:
def calibrate_by_group(predictions: np.array, probabilities: np.array, group_labels: np.array) -> np.array:
"""
Adjust prediction thresholds to equalize positive rates across groups
"""
target_rate = (predictions == 1).mean()
adjusted_predictions = predictions.copy()
for group in np.unique(group_labels):
mask = group_labels == group
group_probs = probabilities[mask]
# Find threshold that gives target positive rate
threshold = np.percentile(group_probs, 100 * (1 - target_rate))
adjusted_predictions[mask] = (group_probs >= threshold).astype(int)
return adjusted_predictions
What We’ve Built
This entire framework is implemented in Vishwas, our AI trust and fairness platform.
Vishwas provides:
- Indian demographic inference for audit purposes
- Pre-built fairness metrics across Indian-relevant dimensions
- Intersectional analysis with statistical significance testing
- Proxy variable detection
- Compliance-ready reporting for RBI, SEBI, and IRDAI requirements
- Integration with Guardian for continuous fairness monitoring
We’ve calibrated our tools on Indian data - not adapted Western tools with a translation layer.
The Regulatory Landscape
Indian regulators are increasingly focused on AI fairness:
RBI: Master Direction on Information Technology Governance requires “fairness and non-discrimination” in customer-facing AI.
SEBI: Guidelines on algo trading mention bias as a risk factor requiring monitoring.
DPDP Act: While focused on privacy, creates data access rights that enable fairness auditing.
Proposed AI regulations: Draft AI governance frameworks explicitly mention non-discrimination requirements.
Enterprises that build fairness infrastructure now will be better positioned for coming regulations.
Getting Started
If you’re deploying AI that affects Indian users:
-
Audit your current models: Use the framework above to check for disparities you might have missed.
-
Don’t rely on Western tools alone: They’re a starting point, not a complete solution.
-
Look at intersections: Single-dimension analysis misses compounded discrimination.
-
Document everything: Regulators will ask. Having audit trails matters.
-
Build fairness into development: Catching bias in production is expensive. Catch it earlier.
Fairness isn’t just ethics - it’s risk management. Biased AI systems face regulatory action, reputational damage, and legal liability.
Contact us to discuss fairness auditing for your AI systems.