機械学習モデルの公平性評価（Demographic Parity）

# はじめに
機械学習モデルを公平性の観点から評価します。  
Jupyter Notebookは下記にあります。

https://github.com/toshi-4886/ML_fairness/blob/main/group_fairness/1_demographic_parity.ipynb


### 概要
- 収入を予測するモデルの予測が、性別によらず公平かどうかを評価します。  
- 公平性指標としてDemographic Parityを使用します。
- 公平性指標を考慮してモデルの学習を行った場合の公平性も評価します。

# 公平性指標  
### Demographic Parity  
今回考える公平性指標は、性別や人種などの各グループが公平に扱われているか（group fairness）を評価するために用いられます。  
Demographic Parityは、グループによらず各クラスが予測される確率が同じになっているかを評価する指標です。  
例えば、採用を判定するモデルで、男性と女性で採用確率が同じになっているかを評価する際に用いられます。

数式で書くと、性別や人種などの属性の集合を$A$、モデルが予測するクラスの集合を$\mathcal{Y}$、属性が$a \in A$のグループがクラス$\hat{y} \in \mathcal{Y}$と予測される確率を$P(\hat{y}|A=a)$とすると、任意の$\hat{y}, a, a'$で下記が成り立つ場合、モデルはDemographic Parityを満たします。

$$
P(\hat{y}|A=a) = P(\hat{y}|A=a')
$$

# 実装

### 1. ライブラリのインポート
必要なライブラリをインポートします。

```python
import sys
import os
from tqdm import tqdm
import matplotlib.pyplot as plt

import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

import lightgbm as lgb

!pip install fairlearn
from fairlearn.datasets import fetch_adult
from fairlearn.reductions import DemographicParity, ExponentiatedGradient
from fairlearn.metrics import demographic_parity_difference, MetricFrame, selection_rate, count
```

### 2. データセットの用意  
今回は[Adult Data Set](http://archive.ics.uci.edu/ml/datasets/adult)を使用します。  
年齢や教育歴などの14の属性から、収入が5万ドルを超えるかどうかを予測するタスクのデータセットで、48,842サンプルが含まれます。  
UCI Machine Learning Repositoryからダウンロードすることもできますが、Fairlearnのfetch_adultで取得します。

```python
X, y = fetch_adult(return_X_y=True, as_frame=True)
print(X.head())
print(X.shape)
print(set(y))
```

```terminal:実行結果
    age  workclass    fnlwgt     education  education-num      marital-status  \
0  25.0    Private  226802.0          11th            7.0       Never-married   
1  38.0    Private   89814.0       HS-grad            9.0  Married-civ-spouse   
2  28.0  Local-gov  336951.0    Assoc-acdm           12.0  Married-civ-spouse   
3  44.0    Private  160323.0  Some-college           10.0  Married-civ-spouse   
4  18.0        NaN  103497.0  Some-college           10.0       Never-married   

          occupation relationship   race     sex  capital-gain  capital-loss  \
0  Machine-op-inspct    Own-child  Black    Male           0.0           0.0   
1    Farming-fishing      Husband  White    Male           0.0           0.0   
2    Protective-serv      Husband  White    Male           0.0           0.0   
3  Machine-op-inspct      Husband  Black    Male        7688.0           0.0   
4                NaN    Own-child  White  Female           0.0           0.0   

   hours-per-week native-country  
0            40.0  United-States  
1            50.0  United-States  
2            40.0  United-States  
3            40.0  United-States  
4            30.0  United-States  
(48842, 14)
{'>50K', '<=50K'}
```

データを7:3にランダムに分割して、それぞれを教師データとテストデータとして使用します。  
ラベルは、Fairlearnで用いられるように'>50K'は1、'<=50K'は0となるように変換しておきます。  

```python
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.7, random_state=42)
y_train = (y_train == '>50K') * 1
y_test = (y_test == '>50K') * 1
```

### 3. 学習  
LightGBMでモデルの学習を行います。  
テスト精度も確認しておきます。

```python
lgb_params = {
    'objective' : 'binary',
    'metric' : 'accuracy',
}
model = lgb.LGBMClassifier(**lgb_params)
model.fit(X_train, y_train)

pred = model.predict(X_test)
print('test accuracy', accuracy_score(y_test, pred))
```

```terminal:実行結果
test accuracy 0.8799563229372824
```

### 4. 公平性評価
Fairlearnのdemographic_parity_differenceで、性別ごとのグループ間の不公平度を計算します。  
この関数は、sensitive_featuresに指定した属性のグループごとに、予測が1となる確率（selection rate）を計算し、グループ間の差の最大値を出力します。

```python
demographic_parity_difference(y_test, pred, sensitive_features=X_test['sex'])
```
```terminal:実行結果
0.1862287222272065
```

グループごとの精度、selection rate、サンプル数をMetricFrameを用いて確認します。  
selection rateの差がdemographic_parity_differenceの値と一致していることも確認できます。  

```python
metric_frame = MetricFrame(
    metrics={
        "accuracy": accuracy_score,
        "selection rate": selection_rate,
        "count": count,
    },
    sensitive_features=X_test['sex'],
    y_true=y_test,
    y_pred=pred,
)
print(metric_frame.overall)
print(metric_frame.by_group)
metric_frame.by_group.plot.bar(
    subplots=True,
    layout=[1, 3],
    legend=False,
    figsize=[12, 3],
)
```

![result_DP.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3225334/8562681b-03b3-e566-7ead-0673c6a2e9d0.png)

### 5. 公平性指標を考慮した学習  
FairlearnのExponentiatedGradientを用いて、公平性が改善するように学習を行います。

```python
model_fair = lgb.LGBMClassifier(**lgb_params)
mitigator = ExponentiatedGradient(model_fair, DemographicParity())
mitigator.fit(X_train, y_train, sensitive_features=X_train['sex'])
pred = mitigator.predict(X_test)
print('test accuracy', accuracy_score(y_test, pred))
```

```terminal:実行結果
test accuracy 0.8579813007575241
```

先ほどと同様に公平性の評価を行います。

```python
print('demographic parity difference', demographic_parity_difference(y_test, pred, sensitive_features=X_test['sex']))
metric_frame = MetricFrame(
    metrics={
        "accuracy": accuracy_score,
        "selection rate": selection_rate,
        "count": count,
    },
    sensitive_features=X_test['sex'],
    y_true=y_test,
    y_pred=pred,
)
print(metric_frame.overall)
print(metric_frame.by_group)
metric_frame.by_group.plot.bar(
    subplots=True,
    layout=[1, 3],
    legend=False,
    figsize=[12, 3],
)
```
![result_DP_fair.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3225334/7d60671e-60ff-d7d3-5a5b-e67ceacbd3d6.png)

# おわりに

### 今回の結果
通常の学習では、Demographic Parityが満たされないこと、公平性を考慮した学習を行うことで、公平性を改善できることが確認できました。  
一方で、テスト精度は、公平性を考慮することで0.87から0.85に少し低下しています。  
公平性とテスト精度を両立できるかも重要な観点となります。  

### 次にやること
グループによらず収入が等しいとは限らないため、高収入と予測される確率を等しくするDemographic Parityに違和感を覚える方も多いと思います。  
別の公平性指標も検証したいと思います。

### 参考資料
- Adult Data Set  
http://archive.ics.uci.edu/ml/datasets/adult
- Fairlearn  
https://fairlearn.org/v0.8/quickstart.html