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:

  1. Audit your current models: Use the framework above to check for disparities you might have missed.

  2. Don’t rely on Western tools alone: They’re a starting point, not a complete solution.

  3. Look at intersections: Single-dimension analysis misses compounded discrimination.

  4. Document everything: Regulators will ask. Having audit trails matters.

  5. 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.