# Notebook from Wang-Cankun/lisa2
Path: data_conversion/mm10_lisa1_data_conversion.ipynb

import importlib
import lisa
lisa = importlib.reload(lisa)

from lisa.core.data_interface import DataInterface,PACKAGE_PATH
from lisa.core.genome_tools import Genome
from lisa.core.utils import indices_list_to_sparse_array
from lisa.core import data_interface
data_interface = importlib.reload(data_interface)

import configparser
import os
import pandas as pd
import numpy as np
import h5py as h5
import seaborn as sns_____no_output_____species = 'mm10'_____no_output_____old_data_path = os.path.join('data_conversion','old_data',species)

v1_config = configparser.ConfigParser()
v1_config.read(os.path.join('data_conversion', 'lisa1_data.ini'))_____no_output_____

# Step 1:

Go to NCBI Table browser and select the UCSC refGene table for mm10, output all fields. Download this table as "mm10.refseq"_____no_output_____

!ls data_conversion/old_data/hg38                  lisa_v1.2_hg38.tar.gz mm10
hg38.refseq           lisa_v1.2_mm10.tar.gz mm10.refseq
tad_info = pd.read_csv(v1_config.get(species, 'tad_info').format(prefix = old_data_path),
                        sep = '\t')
tad_info['tad_group'] = tad_info.k4me3_order_cluster.astype(str) + '-' + tad_info.tad_order_cluster.astype(str)_____no_output_____genome = pd.read_csv(os.path.join(PACKAGE_PATH, 'genomes',species + '.genome'), sep = '\t', header = None)
genome.columns = ['chrom','length']_____no_output_____genes = pd.read_csv(os.path.join('data_conversion', 'old_data', species + '.refseq'), sep = '\t')_____no_output_____genes = genes.merge(tad_info[['geneName','tad_group']], left_on='name2',
            right_on='geneName', how = 'left').fillna('None')_____no_output_____genes.merge(genome['chrom'], on = 'chrom', how = 'right')\
    [['name','chrom','strand','txStart','txEnd','exonStarts','exonEnds','name2', 'tad_group']]\
    .to_csv(os.path.join(PACKAGE_PATH,'genomes',species + '.refseq'), header = None, index = None, sep = '\t')_____no_output_____refseq=os.path.join(PACKAGE_PATH,'genomes',species + '.refseq')_____no_output_____

# Step 2:

Download old LISA data:

```
wget -c http://lisa.cistrome.org/cistromedb_data/lisa_v1.2_mm10.tar.gz
```
and unpack into ./old_data/_____no_output_____# Step 3:

Instantiate a data inferface using the new genome and genes, then use it to generate RP maps_____no_output_____

data = data_interface.DataInterface('mm10', window_size=1000, make_new=True)Loading gene info ...
basic_rp_map, enhanced_rp_map = data.build_binned_rp_map('basic',10000), data.build_binned_rp_map('enhanced', 10000)_____no_output_____data.add_rp_map('basic_10K', basic_rp_map)
data.add_rp_map('enhanced_10K', enhanced_rp_map)_____no_output_____

# Step 4:

Load the old genome format and map to the new bins_____no_output_____

old_genome = Genome.from_file(v1_config.get(species,'chrom_len').format(prefix = old_data_path),
                             window_size=1000, _sort=False)

genome_map = old_genome.map_genomes(data.genome)
genome_map = np.array(genome_map)_____no_output_____

Load in old metadata_____no_output_____

metadata = pd.read_csv(v1_config.get('basics','meta').format(prefix = old_data_path), encoding='latin', sep = '\t')\
    .set_index('id')
metadata.index = metadata.index.astype(str)
motif_meta = pd.read_csv(v1_config.get('basics','motif').format(prefix = old_data_path), encoding='latin', sep = '\t')\
    .set_index('id')
motif_meta['factor'] = motif_meta.symbol_____no_output_____

# Step 5:

Transfer accessibility arrays to new format_____no_output_____

def load_accessibility_data(technology, old_data_path):

    with h5.File(v1_config.get(species, technology + '_count').format(prefix = old_data_path), 'r') as old_data:
        dataset_ids = np.array(old_data['IDs'][...]).astype(str)
        acc_matrix = np.array(old_data['OrderCount'][...])

    return acc_matrix, dataset_ids

def add_accessibility_matrix(data, technology, acc_matrix, dataset_ids,*, rp_maps, rp_map_styles, metadata,
                            reads_range):

    headers = data.get_metadata_headers(technology)

    for i, dataset_id in enumerate(dataset_ids):

        dataset_id = str(dataset_id).split('_')[0]
        sample_metadata = metadata.loc[dataset_id, headers + ['qc']].to_dict()

        sample_acc = np.array(acc_matrix[:, i]).reshape(-1)

        if int(sample_metadata['qc']) == 1 and (reads_range[0] <= sample_acc.sum() <= reads_range[1]):
            data.add_profile_data(technology, str(dataset_id), sample_acc, rp_maps,
                                      rp_map_styles, **sample_metadata)

def convert_accessibility(*,data,technology, old_data_path, bin_map,
                          rp_maps, rp_map_styles, metadata, reads_range):

    acc_matrix, dataset_ids = load_accessibility_data(technology, old_data_path)

    projected_acc_matrix = np.vstack([
        data.project_array(r, bin_map, len(data.genome))
        for r in acc_matrix.T
    ]).T

    add_accessibility_matrix(data, technology, projected_acc_matrix, dataset_ids,
                         rp_maps=rp_maps, rp_map_styles=rp_map_styles,
                        metadata = metadata, reads_range = reads_range)_____no_output_____accessibility_kwargs = dict(
    data = data,
    old_data_path=old_data_path,
    bin_map=genome_map, rp_maps = [basic_rp_map, enhanced_rp_map], rp_map_styles = ['basic_10K','enhanced_10K'],
    metadata = metadata
)

convert_accessibility(technology='DNase', **accessibility_kwargs, reads_range = (75000, 160000))

convert_accessibility(technology='H3K27ac', **accessibility_kwargs, reads_range = (130000, 160000))_____no_output_____

# Check for bad data_____no_output_____

import importlib_____no_output_____importlib.reload(data_interface)_____no_output_____data = data_interface.DataInterface(species, window_size=1000)Loading gene info ...
acc_matrix, dataset_ids = load_accessibility_data('H3K27ac', old_data_path)_____no_output_____acc_sums = acc_matrix.sum(axis = 0)_____no_output_____sns.histplot(acc_sums)_____no_output_____dataset_ids = np.array([x.split('_')[0] for x in dataset_ids])_____no_output_____bad_samples = dataset_ids[~np.logical_and(acc_sums >= 130000, acc_sums <= 160000, metadata.loc[dataset_ids].qc.values.astype(np.bool))]_____no_output_____profiles = data.list_profiles('H3K27ac')_____no_output_____for bad_samp in bad_samples:
    data.remove_profile('H3K27ac', bad_samp)_____no_output_____profiles = data.list_profiles('DNase')_____no_output_____

# Step 6:

Transfer factor binding to new genome scale_____no_output_____

old_genome_100bp = Genome.from_file(v1_config.get(species,'chrom_len').format(prefix = old_data_path),
                             window_size=100, _sort=False)

hits_binmap = np.array(old_genome_100bp.map_genomes(data.genome))_____no_output_____def load_tf_matrix(config_key, old_data_path, num_bins, metadata, offset = 0, skip_qc = False):

    with h5.File(v1_config.get(species, config_key)\
                 .format(prefix = old_data_path), 'r') as tf_hits:

        num_samples = len(list(tf_hits.keys()))

        indices, dataset_ids = [],[]

        for sample in tf_hits.keys():

            if not sample == 'IDs' and not sample == 'TFs':

                try:
                    sample_metadata = metadata.loc[sample]

                    if skip_qc or sample_metadata.qc == 1:

                        factor_name = sample_metadata.factor.replace('/', '-').replace('_','-')

                        peaks = tf_hits[sample][...]
                        peaks = peaks - offset

                        indices.append(peaks)
                        dataset_ids.append(sample)

                except OSError:
                    print('\n\tError saving data for sample {}, factor: {}'\
                          .format(str(sample), sample_metadata.factor))
                except KeyError:
                    print('\n\tError: No metadata for sample {}'.format(str(sample)))

        tf_matrix = indices_list_to_sparse_array(indices, num_bins)

        return tf_matrix, dataset_ids

def write_tf_matrix(data, technology, dataset_ids, tf_matrix, metadata):

    tf_matrix = tf_matrix.T.tocsr()

    for i, dataset_id in enumerate(dataset_ids):
        dataset_id = str(dataset_id)
        save_indices = tf_matrix[i, :].indices
        sample_metadata = metadata.loc[dataset_id, data.get_metadata_headers(technology)].to_dict()
        data.add_binding_data(technology, dataset_id, save_indices, **sample_metadata)

def convert_tf_binding(*, data, technology, config_key, old_data_path, original_num_bins, hits_binmap,
                metadata, offset = 0, skip_qc = False):

    tf_matrix, dataset_ids = load_tf_matrix(config_key, old_data_path, original_num_bins,
                                        metadata, offset = offset, skip_qc = skip_qc)

    projected_tf_matrix = data.project_sparse_matrix(tf_matrix.T, hits_binmap, len(data.genome))

    write_tf_matrix(data, technology, dataset_ids, projected_tf_matrix, metadata)_____no_output_____

### ChIP-seq data_____no_output_____

convert_tf_binding(data = data, technology = 'ChIP-seq', config_key = 'tf_chipseq', old_data_path = old_data_path,
                  original_num_bins = len(old_genome_100bp), metadata = metadata, offset = 1, skip_qc = False,
                  hits_binmap = hits_binmap)_____no_output_____

### Motif data_____no_output_____

convert_tf_binding(data = data, technology = 'Motifs', config_key = 'genome_100bp_motif_index99', old_data_path = old_data_path,
                original_num_bins = len(old_genome_100bp), metadata = motif_meta, offset = 0, skip_qc = True,
                hits_binmap = hits_binmap)_____no_output_____

# 100bp hits data_____no_output_____

fine_hitbin_data = data_interface.DataInterface(species, window_size=100, make_new=True)Loading gene info ...
hits_fine_binmap = np.array(old_genome_100bp.map_genomes(fine_hitbin_data.genome))_____no_output_____convert_tf_binding(data = fine_hitbin_data, technology = 'ChIP-seq', config_key = 'tf_chipseq', old_data_path = old_data_path,
                  original_num_bins = len(old_genome_100bp), metadata = metadata, offset = 1, skip_qc = False,
                  hits_binmap = hits_fine_binmap)_____no_output_____convert_tf_binding(data = fine_hitbin_data, technology = 'Motifs', config_key = 'genome_100bp_motif_index99', old_data_path = old_data_path,
                original_num_bins = len(old_genome_100bp), metadata = motif_meta, offset = 0, skip_qc = True,
                hits_binmap = hits_fine_binmap)_____no_output_____

# Motifs mapping correctly?_____no_output_____

matrix, dataset_ids, metadata = fine_hitbin_data.get_binding_data('Motifs')_____no_output_____factor_metadata = pd.DataFrame(metadata)_____no_output_____factor_metadata[factor_metadata.factor == 'RUNX1']_____no_output_____np.where(dataset_ids == 'M00082')_____no_output_____dataset_ids[:5]_____no_output_____with open('data_conversion/runx1_motif_windows.bed', 'w') as f:
    for ind in matrix[:, 3].tocoo().row[:1000]:
        ind = int(ind)
        print(fine_hitbin_data.genome.get_region(ind)[0], file = f)    _____no_output_____!head data_conversion/runx1_motif_windows.bedchr1	3007900	3008000
chr1	3047900	3048000
chr1	3095500	3095600
chr1	3120400	3120500
chr1	3130600	3130700
chr1	3134200	3134300
chr1	3195700	3195800
chr1	3212300	3212400
chr1	3217000	3217100
chr1	3219800	3219900


# Notebook from dgiova/fairlearn
Path: notebooks/Mitigating_Gender_Disparities_in_GPA_Data_v2.ipynb

import numpy as np
import pandas as pd
from IPython.display import display, HTML
import matplotlib.pyplot as plt
from scipy.stats import cumfreq
import sklearn.metrics as skm

from sklearn.calibration import CalibratedClassifierCV
import sklearn.metrics as skm
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression, LogisticRegression
from sklearn.ensemble import RandomForestRegressor
from sklearn.neural_network import MLPRegressor_____no_output_____import fairlearn.metrics as flm
from fairlearn.reductions import ExponentiatedGradient, GridSearch, DemographicParity, EqualizedOdds, GroupLossMoment, Moment, SquareLoss
from fairlearn.metrics import group_mean_prediction, group_mean_squared_error, group_mean_overprediction, group_roc_auc_score, group_accuracy_score
from fairlearn.metrics import group_roc_auc_score_____no_output_____from fairlearn.metrics import group_mean_underprediction_____no_output_____

# Helper Functions _____no_output_____

def split_dataframe(split, X, y, A, frac=True):
  if frac==True:
    random_idx = np.random.random_integers(0, len(X), int(split*len(X)))
  else:
    random_idx = np.random.random_integers(0, len(X), split)
  X_random = X.iloc[random_idx]
  y_random = y.iloc[random_idx]
  A_random = A.iloc[random_idx]

  return X_random, y_random, A_random_____no_output_____def loss_fct(i):
  return grid_search.objective_weight * grid_search._objectives[i] + \
  grid_search.constraint_weight * grid_search._gammas[i].max()_____no_output_____def group_metric_as_df(name, group_metric_result):
    a = pd.Series(group_metric_result.by_group)
    a['overall'] = group_metric_result.overall
    return pd.DataFrame({name: a})_____no_output_____def plot_data(Xs, Ys, A):
  print(Xs.shape, Ys.shape, A.shape)
  labels = np.unique(A)

  for l in labels:
    label_string = str(l.item())
    mask =  A== l
    plt.scatter(Xs[mask].mean(axis=1), Ys[mask], label=str("Label="+label_string))
    plt.xlabel("GPA")
    plt.ylabel("Higher or not higher than 3.0 ")

  plt.legend()
  plt.show()_____no_output_____def compare_cdfs(data, A, num_bins=100):
    cdfs = {}
    assert len(np.unique(A)) == 2

    limits = ( min(data), max(data) )
    s = 0.5 * (limits[1] - limits[0]) / (num_bins - 1)
    limits = ( limits[0]-s, limits[1] + s)

    for a in np.unique(A):
        subset = data[A==a]

        cdfs[a] = cumfreq(subset, numbins=num_bins, defaultreallimits=limits)

    lower_limits = [v.lowerlimit for _, v in cdfs.items()]
    bin_sizes = [v.binsize for _,v in cdfs.items()]
    actual_num_bins = [v.cumcount.size for _,v in cdfs.items()]

    assert len(np.unique(lower_limits)) == 1
    assert len(np.unique(bin_sizes)) == 1
    assert np.all([num_bins==v.cumcount.size for _,v in cdfs.items()])

    xs = lower_limits[0] + np.linspace(0, bin_sizes[0]*num_bins, num_bins)

    disparities = np.zeros(num_bins)
    for i in range(num_bins):
        cdf_values = np.clip([v.cumcount[i]/len(data[A==k]) for k,v in cdfs.items()],0,1)
        disparities[i] = max(cdf_values)-min(cdf_values)

    return xs, cdfs, disparities

def plot_and_compare_cdfs(data, A, num_bins=100, loc='best'):
    xs, cdfs, disparities = compare_cdfs(data, A, num_bins)

    for k, v in cdfs.items():
        plt.plot(xs, v.cumcount/len(data[A==k]), label=k)

    assert disparities.argmax().size == 1
    d_idx = disparities.argmax()

    xs_line = [xs[d_idx],xs[d_idx]]
    counts = [v.cumcount[d_idx]/len(data[A==k]) for k, v in cdfs.items()]
    ys_line = [min(counts), max(counts)]

    plt.plot(xs_line, ys_line, 'o--')
    disparity_label = "max disparity = {0:.3f}\nat {1:0.3f}".format(disparities[d_idx], xs[d_idx])
    plt.text(xs[d_idx], 1, disparity_label, ha="right", va="top")

    plt.xlabel(data.name)
    plt.ylabel("cumulative frequency")
    plt.legend(loc=loc)
    plt.show()_____no_output_____# show_proportions is only a helper function for plotting
def show_proportions(X, sensitive_features, y_pred, y=None, description=None, plot_row_index=1):
    print("\n" + description)
    plt.figure(plot_row_index)
    plt.title(description)
    plt.ylabel("P[3.0 GPA predicted | gender]")

    indices = {}
    positive_indices = {}
    negative_indices = {}
    recidivism_count = {}
    recidivism_pct = {}
    groups = np.unique(sensitive_features.values)
    n_groups = len(groups)
    color = plt.cm.rainbow(np.linspace(0,1,n_groups))
    x_tick_labels_basic = []
    x_tick_labels_by_label = []
    for index, group in enumerate(groups):
        indices[group] = sensitive_features.index[sensitive_features == group]
        recidivism_count[group] = sum(y_pred[indices[group]])
        recidivism_pct[group] = recidivism_count[group]/len(indices[group])
        plt.bar(index + 1, recidivism_pct[group], color=color[index])
        x_tick_labels_basic.append(group)

        if y is not None:
            positive_indices[group] = sensitive_features.index[(sensitive_features == group) & (y == 1)]
            negative_indices[group] = sensitive_features.index[(sensitive_features == group) & (y == 0)]
            prob_1 = sum(y_pred[positive_indices[group]])/len(positive_indices[group])
            prob_0 = sum(y_pred[negative_indices[group]])/len(negative_indices[group])

            plt.bar(n_groups + 1 + 2 * index, prob_1, color=color[index])
            plt.bar(n_groups + 2 + 2 * index, prob_0, color=color[index])
            x_tick_labels_by_label.extend(["{} GPA > 3.0 ".format(group), "{} GPA < 3.0".format(group)])

    x_tick_labels = x_tick_labels_basic + x_tick_labels_by_label
    plt.xticks(range(1, len(x_tick_labels)+1), x_tick_labels, rotation=45, horizontalalignment="right")_____no_output_____

# Introduction_____no_output_____We consider the task of predicting student GPA during their first three semesters of university based on their entrance exams. The fairness across gender was explored in the paper ["Preventing undesirable behavior of intelligent machines" (Thomas et. al 2019) ](https://science.sciencemag.org/content/366/6468/999) We highlight some of the fairness considerations that come up in GPA prediction prediction by gender.

The [dataset](https://dataverse.harvard.edu/dataset.xhtml?persistentId=doi:10.7910/DVN/O35FW8#) contains the performance of 43,303 students from Brazil on their nine entrance exams for the Federal University of Rio Grande do Sul and their GPA over their first three semesters of university. Each row contains anonymized data on one student's scores on nine exams (phsics, biology, history, second language, geography, literature, Portuguese essay, math, chemistry) along with a boolean indicating their gender (0 female, 1 male). The outcome is a numerical value representing their average GPA on a 4.0 scale during the first three months of university._____no_output_____# Exploratory Data Analysis _____no_output_____

names = ['gender', 'physics','biology','history','second_language',
'geography','literature','portuguese', 'math','chemistry', 'gpa']
data = pd.read_csv("data.csv", names=names)_____no_output_____all_train_grouped = data.groupby('gender')

counts_by_gender = all_train_grouped[['gpa']].count().rename(
    columns={'gpa': 'count'})

quartiles_by_gender = all_train_grouped[['gpa']].quantile([.25, .50, .75]).rename(
    index={0.25: "25%", 0.5: "50%", 0.75: "75%"}, level=1).unstack()

summary_by_gender = pd.concat([counts_by_gender, quartiles_by_gender], axis=1)

summary_by_gender.rename(index={0: "female", 1: "male"}, inplace=True)
summary_by_gender.loc['Difference']=(summary_by_gender.iloc[0]-summary_by_gender.iloc[1]).abs()
display(summary_by_gender)_____no_output_____

We see there is a similar distribution between male and female students in the data (There are 6.5% more males samples than females).

We see that the GPA of female students is higher than male across all quartiles. The difference across the GPAs is smallest in the 75th percentile (0.27 difference). However the gap is much larger in the 50th and 25th percentile._____no_output_____

gender_gpa = data[["gender", "gpa"]].copy()
gender_gpa.replace({'gender': {0:"female", 1:"male"}}, inplace=True)
fig, axes = plt.subplots(nrows=1, ncols=2, sharex=True, sharey=True)
for i in range(len(axes)):
  axes[i].set(xlabel='GPA')
  if i==0:
    axes[i].set(ylabel='Number of students')
gender_gpa.hist(by='gender', column='gpa', ax = axes)
# Add title and axis names
plt.suptitle('GPA Distribution by Gender', x=0.5, y=1.05, ha='center', fontsize='xx-large')
plt.show()_____no_output_____

We see that the distribution of GPAs for females skews much higher for females than males. Females have fewer students with lower GPAs and more students with GPAs in the 3-4 range. Male students have on average lower GPAs with an equal number of students in the 2-3.5 range.
_____no_output_____

all_train_grouped = data.groupby('gender').mean()
all_train_grouped.rename(index={0: "female", 1: "male"}, inplace=True)
all_train_grouped.gpa.plot(kind='bar', title='Mean GPA per gender')
pct_greater = (all_train_grouped.gpa.loc['female'] - all_train_grouped.gpa.loc['male'])/ all_train_grouped.gpa.loc['male']
print('True Percent Difference of Female GPA over Male GPA')
print(round(pct_greater*100,2), '%')True Percent Difference of Female GPA over Male GPA
13.58 %

We see that the Mean GPA across all samples is higher for females by 13.58%._____no_output_____

display(all_train_grouped)
test_scores_grouped = all_train_grouped.transpose().iloc[0:9,]
test_scores_grouped.plot(kind='bar', figsize=(13,7), title='Mean exam score per gender')_____no_output_____

# Model Performance Comparisons for ML Algos with & without Fairness Constraints_____no_output_____## Mean GPA Prediction Error Comparisons _____no_output_____We now explore the level of fairness of three standard ML algorithms: Least Squares, Random Forest, Multi-layer Perceptron regressors.

For each of these, the pipeline will consist of:
- Remove the protected attribute from the predictors. This is an essential step that allows to analyze whether the algorithm has an inner bias toward a specific group, female or male in our study.
- Split the dataset in train-test in a 4:1 ratio, train the model on the train data and predict on the test data._____no_output_____Specifically, we'll have the following data subgroups:

*   **X_train, X_test**: features describing the training and test data; the scores on 9 entrance exams
*   **A_train, A_test**:  gender of each student (female or male)
*   **y_train, y_test**: labels of the training and test data; the labels are numerical on the scale [0,4.0] representing the GPA during the first three months of University
*   **y_train_bool, y_test_bool**: binary labels of the training and test data; the labels are boolean; 1 if gpa > 3.0; 0 otherwise

_____no_output_____

X = data.iloc[:,:-1]
y = data.gpa
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2,
                                                    random_state=42)
A_train = X_train.gender
A_test = X_test.gender
del X_train['gender']
del X_test['gender']

y_train_bool = y_train > 3.0
y_test_bool = y_test > 3.0

X_train.reset_index(inplace = True, drop = True)
y_train.reset_index(inplace = True, drop = True)
y_train_bool.reset_index(inplace = True, drop = True)
A_train.reset_index(inplace = True, drop = True)
X_test.reset_index(inplace = True, drop = True)
y_test.reset_index(inplace = True, drop = True)
A_test.reset_index(inplace = True, drop = True)
y_test_bool.reset_index(inplace = True, drop = True)_____no_output_____# Least Squares
lr = LinearRegression().fit(X_train, y_train)
lr_pred = lr.predict(X_test)_____no_output_____

A multilayer perceptron (MLP) is a class of feedforward artificial neural network (ANN). Except for the input nodes, each node is a neuron that uses a nonlinear activation function. MLP utilizes a supervised learning technique called backpropagation for training._____no_output_____

# Multi-layer Perceptron
mlp = MLPRegressor(early_stopping=True,
                   activation="relu",
                   random_state=42).fit(X_train, y_train)
mlp_pred = mlp.predict(X_test)_____no_output_____

A random forest is a meta estimator that fits a number of classifying decision trees on various sub-samples of the dataset and uses averaging to improve the predictive accuracy and control over-fitting. The sub-sample size is always the same as the original input sample size but the samples are drawn with replacement._____no_output_____

# Random Forest
rf = RandomForestRegressor(n_estimators=10, max_depth=5).fit(X_train, y_train)
rf_pred = rf.predict(X_test)_____no_output_____

The next step consists in analyzing whether any of these alogirthms shows a consistent bias toward one gender.

To do that, we divided the prediction output in two groups, female and male, and then computed the **mean prediction error** for each group.

- Compute error as: `y_pred - y_observed`
- Compute mean error grouped by gender_____no_output_____

error_df = pd.DataFrame(A_test)
gender_map = {0: 'female',
              1: 'male'}
error_df['gender'] = error_df.gender.map(gender_map)
error_df['lr_pred'] = lr_pred - y_test
error_df['mlp_pred'] = mlp_pred - y_test
error_df['rf_pred'] = rf_pred - y_test
fig = error_df.groupby('gender').mean().rename(columns={'lr_pred':'LR',
                                                  'mlp_pred': 'ANN',
                                                  'rf_pred': 'RF'}
                                         ).T.plot(kind='bar',
                                                  figsize=(11,7),
                                                  title='Mean prediction error by gender',
                                                  colormap='binary',
                                                  linewidth=1,
                                                  edgecolor='black')
fig.set(xlabel="Regression Algorithm", ylabel="Prediction Error")_____no_output_____

The user-selected definition of undesirable behavior corresponds to large differences in mean prediction errors (mean predicted GPA minus mean observed GPA) for applicants of **different genders**.

This plot shows the mean prediction errors for male and female students when using each regression algorithm.

All shown standard ML methods tend to notably overpredict the performance of male students and underpredict the performance of female students_____no_output_____## Mean Squared Error and Accuracy Rate Using GridSearch and Exponentiated Gradient _____no_output_____

import random
from sklearn.preprocessing import MinMaxScaler
import sklearn.metrics as skm
from fairlearn.metrics import group_mean_prediction, group_mean_squared_error, group_mean_overprediction
import fairlearn.metrics as flm _____no_output_____

### Data Processing_____no_output_____

def prob_decision(p):
    return 1*(random.random()
 if zi > 0.5 then yi = 0 <-> if zi < 0.5 then yi = 1
y_train_scaled = pd.Series(y_train_rescaled.reshape(-1))
y_train_binary = y_train_scaled.apply(prob_decision)_____no_output_____

### 1. Baseline: Logistic Regression Model without Fairness Constraints_____no_output_____

unmitigated_predictor_rescaled = LogisticRegression(solver='liblinear', fit_intercept=True)
unmitigated_predictor_rescaled.fit(X_train, y_train_binary)
scores_unmitigated_rescaled = pd.Series(unmitigated_predictor_rescaled.predict_proba(X_test)[:,1], name="score_unmitigated_rescaled")
_____no_output_____mse_unmitigated = group_metric_as_df("mse_unmitigated_rescaled", group_mean_squared_error(y_test_rescaled, scores_unmitigated_rescaled, A_test))
mse_unmitigated.rename(index={0: "female", 1: "male"}, inplace=True)
display(mse_unmitigated)_____no_output_____mse_unmitigated_overprediction = group_metric_as_df("mse_unmitigated_overprediction", group_mean_overprediction(y_test_rescaled, scores_unmitigated_rescaled, A_test))
mse_unmitigated_overprediction.rename(index={0: "female", 1: "male"}, inplace=True)
display(mse_unmitigated_overprediction)_____no_output_____mse_unmitigated_underprediction = group_metric_as_df("mse_unmitigated_underprediction", group_mean_underprediction(y_test_rescaled, scores_unmitigated_rescaled, A_test))
mse_unmitigated_underprediction.rename(index={0: "female", 1: "male"}, inplace=True)
display(mse_unmitigated_underprediction)_____no_output_____

### 2. GridSearch: Logistic Regression Model with Equalized Odds as Fairness Constraint _____no_output_____

sweep_gs_rescaled = GridSearch(LogisticRegression(solver='liblinear', fit_intercept=True),
                   constraints=EqualizedOdds(),
                   grid_size=50,
                   grid_limit=10)
sweep_gs_rescaled.fit(X_train, y_train_binary, sensitive_features=A_train)
best_results_gs_rescaled = sweep_gs_rescaled.predict_proba(X_test)_____no_output_____scores_best_gs_rescaled = pd.Series(best_results_gs_rescaled[:,1], name="score_best_gs_rescaled")_____no_output_____mse_gridsearch = group_metric_as_df("mse_gridsearch_eo", group_mean_squared_error(y_test_rescaled, scores_best_gs_rescaled, A_test))
mse_gridsearch.rename(index={0: "female", 1: "male"}, inplace=True)
display(mse_gridsearch)_____no_output_____mse_gridsearch_overprediction = group_metric_as_df("mse_gridsearch_overprediction", group_mean_overprediction(y_test_rescaled, scores_best_dp_rescaled, A_test))
mse_gridsearch_overprediction.rename(index={0: "female", 1: "male"}, inplace=True)
display(mse_gridsearch_overprediction)_____no_output_____mse_gridsearch_underprediction = group_metric_as_df("mse_gridsearch_underprediction", group_mean_underprediction(y_test_rescaled, scores_best_dp_rescaled, A_test))
mse_gridsearch_underprediction.rename(index={0: "female", 1: "male"}, inplace=True)
display(mse_gridsearch_underprediction)_____no_output_____### TEST WITH DIFFERENT GRID SIZES
# sweep_gs_test = GridSearch(LogisticRegression(solver='liblinear', fit_intercept=True),
#                    constraints=EqualizedOdds(),
#                    grid_size=100,
#                    grid_limit=10)
# sweep_gs_test.fit(X_train, y_train_binary, sensitive_features=A_train)_____no_output_____

### 3. Exponentiated Gradient: Logistic Regression Model with Equalized Odds as Fairness Constraint _____no_output_____

# EO without balanced dataset

# tried: eps=0.01, 0.05 and nu 1e-6, 1e-5;
expgrad_eo = ExponentiatedGradient(
    LogisticRegression(solver='liblinear', fit_intercept=True),
    constraints=EqualizedOdds(),
    eps=0.01,
    nu=1e-6)
expgrad_eo.fit(
    X_train,
    y_train_binary,
    sensitive_features=A_train)_____no_output_____scores_expgrad_eo = pd.Series(expgrad_eo.predict(X_test), name="scores_expgrad_eo_rescaled")_____no_output_____for score in scores_expgrad_eo:
    if score!= 1:
        print(score)_____no_output_____mse_expgrad_overprediction = group_metric_as_df("mse_expgrad_eo_overprediction", group_mean_overprediction(y_test_rescaled, scores_expgrad_eo, A_test))
mse_expgrad_overprediction.rename(index={0: "female", 1: "male"}, inplace=True)
display(mse_expgrad_overprediction)_____no_output_____mse_expgrad_underprediction = group_metric_as_df("mse_expgrad_eo_underprediction", group_mean_underprediction(y_test_rescaled, scores_expgrad_eo, A_test))
mse_expgrad_underprediction.rename(index={0: "female", 1: "male"}, inplace=True)
display(mse_expgrad_underprediction)_____no_output_____# EO with balanced dataset
balanced_index_pass0 = y_train_binary[y_train_binary==0].index
balanced_index_pass1 = y_train_binary[y_train_binary==1].sample(n=balanced_index_pass0.size, random_state=0).index
balanced_index = balanced_index_pass0.union(balanced_index_pass1)

expgrad_eo_balanced = ExponentiatedGradient(
    LogisticRegression(solver='liblinear', fit_intercept=True),
    constraints=EqualizedOdds(),
    eps=0.01,
    nu=1e-6)

expgrad_eo_balanced.fit(
    X_train.iloc[balanced_index,:],
    y_train_binary.iloc[balanced_index],
    sensitive_features=A_train.iloc[balanced_index])_____no_output_____scores_expgrad_eo_balanced = pd.Series(expgrad_eo_balanced.predict(X_test), name="scores_expgrad_eo_balanced")_____no_output_____mse_expgrad_balanced_overprediction = group_metric_as_df("mse_expgrad_eo_balanced_overprediction", group_mean_overprediction(y_test_rescaled, scores_expgrad_eo_balanced, A_test))
mse_expgrad_balanced_overprediction.rename(index={0: "female", 1: "male"}, inplace=True)
display(mse_expgrad_balanced_overprediction)_____no_output_____mse_expgrad_balanced_underprediction = group_metric_as_df("mse_expgrad_eo_balanced_underprediction", group_mean_underprediction(y_test_rescaled, scores_expgrad_eo_balanced, A_test))
mse_expgrad_balanced_underprediction.rename(index={0: "female", 1: "male"}, inplace=True)
display(mse_expgrad_balanced_underprediction)_____no_output_____

### Result Analysis: Baseline vs. GridSearch vs. Exponentiated Gradient_____no_output_____### MSE Overprediction and Underprediction _____no_output_____#### Overall MSE Overprediction and Underprediction for Each Model_____no_output_____

mse_overprediction = pd.concat([mse_unmitigated_overprediction, mse_gridsearch_overprediction, mse_expgrad_overprediction, mse_expgrad_balanced_overprediction], axis = 1)
mse_underprediction = pd.concat([mse_unmitigated_underprediction, mse_gridsearch_underprediction, mse_expgrad_underprediction, mse_expgrad_balanced_underprediction], axis = 1)_____no_output_____display(mse_overprediction, mse_underprediction)_____no_output_____mse_concat = pd.concat([mse_overprediction, mse_underprediction], axis=1)
mse_concat.reset_index()_____no_output_____mse_concat.groupby(level=0).mean().T.plot(kind='bar',
                                                  figsize=(11,7),
                                                  title='MSE Overprediction and Underprediction by Gender',
                                                  colormap='binary',
                                                  linewidth=1,
                                                  edgecolor='black')
fig.set(xlabel="Model Type", ylabel="Mean Squared Error")_____no_output_____

#### Difference in MSE Overprediction between the Different Groups
Calculating the percentage difference in MSE overprediction for each gender relative to the "overall" MSE overprediction _____no_output_____

mse_overprediction_diff_female = np.round((mse_overprediction.loc["female"] - mse_overprediction.loc["overall"]) / mse_overprediction.loc["overall"] * 100 , 2)
mse_overprediction_diff_male = np.round((mse_overprediction.loc["male"] - mse_overprediction.loc["overall"]) / mse_overprediction.loc["overall"] * 100, 2)
mse_overprediction_diff = pd.concat([mse_overprediction_diff_female, mse_overprediction_diff_male], axis=1)
mse_overprediction_diff.rename(columns={0:"female", 1:"male"}, inplace=True )_____no_output_____mse_underprediction_diff_female = np.round((mse_underprediction.loc["female"] - mse_underprediction.loc["overall"]) / mse_underprediction.loc["overall"] * 100 , 2)
mse_underprediction_diff_male = np.round((mse_underprediction.loc["male"] - mse_underprediction.loc["overall"]) / mse_underprediction.loc["overall"] * 100, 2)
mse_underprediction_diff = pd.concat([mse_underprediction_diff_female, mse_underprediction_diff_male], axis=1)
mse_underprediction_diff.rename(columns={0:"female", 1:"male"}, inplace=True )_____no_output_____display(mse_overprediction_diff)
display(mse_underprediction_diff)_____no_output_____mse_diff_concat = pd.concat([mse_overprediction_diff, mse_underprediction_diff])
# mse_diff_concat.reset_index(inplace=True)_____no_output_____mse_diff_concat = pd.concat([mse_overprediction_diff, mse_underprediction_diff])
mse_diff_concat.reset_index(inplace=True)
mse_diff_concat.set_index(["female", "male"], inplace=True)_____no_output_____mse_diff_concat.T.groupby(level=0).mean().T.plot(kind='bar',
                                                  figsize=(11,7),
                                                  title='% Difference in MSE Overprediction and Underprediction by Gender Relative to Overall MSE',
                                                  colormap='binary',
                                                  linewidth=1,
                                                  edgecolor='black')
fig.set(xlabel="Model Type", ylabel="Mean Squared Error")_____no_output_____A_test_renamed = A_test.replace([0,1], ["female", "male"])_____no_output_____from fairlearn.widget import FairlearnDashboard

FairlearnDashboard(sensitive_features=A_test_renamed,
                   y_true=y_test,
                   y_pred={"unmitigated": scores_unmitigated_rescaled,
                           "gridsearch": scores_best_gs_rescaled,
                           "expgrad_balanced": scores_expgrad_eo_balanced
                   })_____no_output_____

## Replicating Fig. 3 in "Preventing undesirable behavior of intelligent machines" _____no_output_____In attempt to replicate fig. 3 in ["Preventing undesirable behavior of intelligent machines" (Thomas et. al 2019) ](https://science.sciencemag.org/content/366/6468/999), we further explored the effects of using Grid Search and Exponentiated Gradient methods to impose Fairlearn's Demographic Parity and Equalized Odds fairness constraints on the GPA classification algorithm results._____no_output_____First, we train a standard logistic regression predictor that does not seek to incorporate any notion of fairness. We simply fit this Logistic Regression model to the training data and plot some characteristics of training and test data as well as the predictions of the model on those datasets. _____no_output_____

unmitigated_predictor = LogisticRegression(solver='liblinear', fit_intercept=True)
unmitigated_predictor.fit(X_train, y_train_bool)
predict_unmitigated = unmitigated_predictor.predict(X_test)
scores_unmitigated = pd.Series(unmitigated_predictor.predict_proba(X_test)[:,1], name="score_unmitigated")_____no_output_____A_test_renamed = A_test.replace({0: "female", 1: "male"})_____no_output_____

We interpret the probabilistic predictions outputted by the logistic regression as the scores and evaluate the quality of the GPA classification they produce in terms of the prediction accuracy. We decided to use accuracy as our performance metrics because the dataset is relatively balanced between the two groups, i.e. female and male students.

Before we continue with our analysis, we will provide the definition of the metric used. The accuracy score is, informally, defined as the fraction of predictions our model gets right and formally, defined as the number of correct predictions (TP+TN) divided by total predictions (TP+TN+FP+FN). Although the logistic regression estimator is designed to directly optimize the logistic loss not the accuracy score, a good logistic loss is still expected to give a good accuracy score.

To obtain the accuracy values for the overall student population as well as black and white subpopulations, we use the group metric variant of the sklearn metric accuracy_score._____no_output_____

acc_unmitigated = group_metric_as_df("accuracy_unmitigated",
                                     group_accuracy_score(y_test_bool, predict_unmitigated, A_test))
acc_unmitigated.rename(index={0: "female", 1: "male"}, inplace=True)
display(acc_unmitigated)_____no_output_____show_proportions(X_test, A_test_renamed, y_test_bool, description="original test data:", plot_row_index=3)
plt.show()
original test data:
show_proportions(X_test, A_test_renamed, predict_unmitigated, y_test_bool, description="fairness-unaware prediction on test data:", plot_row_index=4)
plt.show()
fairness-unaware prediction on test data:

We notice that males have approx. 1-2% higher likelihood of getting a predicted GPA above 3.0 whereas women have about 10% lower likelihood of getting assigned a predicted GPA above 3.0._____no_output_____We will now use Fairlearn's Grid Search and Exponentiated Gradient algorithms to impose fairness constraints on the ML algorithms. The goal is to make the output fair with respect to fairness constraints. \\
The Fairlearn algorithms use one of the following fairness constraints:
- Demographic Parity (DP): $P\ [\ h(X)=\hat{y}\ |\ A=a] = P\ [\ h(X)=\hat{y}\ ] \qquad \forall a, \hat{y}$
- Equalized Odds (EO): $P\ [\ h(X)=\hat{y}\ |\ A=a, Y=y] = P\ [\ h(X)=\hat{y}\ |\ Y=y\ ] \qquad \forall a, \hat{y}$

where $h(X)$ is the prediction based on the input $X$, $\hat{y}$ and $y$ are labels, and $a$ is a sensitive feature value. \\

_____no_output_____#### Mitigating Demographic Disparity with Grid Search _____no_output_____We will now use Fairlearn's Grid Search algorithm to mitigate the demographic disparity by building models that achieve various trade-offs between accuracy and demographic disparity.

GridSearch generates models corresponding to various Lagrange multiplier vectors of the underlying constraint optimization problem. We will compute 50 models on a grid of Lagrange multiplier vectors whose L1-norm is bounded by 10. For details on how the search works, refer to Section 3.4 of Agarwal et. al (2018).
_____no_output_____##### Using Demographic Parity as Fairness Constraint_____no_output_____

first_sweep_dp = GridSearch(LogisticRegression(solver='liblinear', fit_intercept=True),
                   constraints=DemographicParity(),
                   grid_size=50,
                   grid_limit=10)
first_sweep_dp.fit(X_train, y_train_bool, sensitive_features=A_train)
Y_first_predict_dp = first_sweep_dp.predict(X_test)
best_results_dp_proba = first_sweep_dp.predict_proba(X_test)
scores_best_dp = pd.Series(best_results_dp_proba[:,1], name="score_best_model")_____no_output_____acc_gs_dp = group_metric_as_df("accuracy_gs_dp",
                                     group_accuracy_score(y_test_bool, Y_first_predict_dp, A_test_renamed))_____no_output_____lambda_vecs_dp = first_sweep_dp._lambda_vecs
actual_multipliers_dp = [lambda_vecs_dp[col][("+", "all", 1)]-lambda_vecs_dp[col][("-", "all", 1)] for col in lambda_vecs_dp]_____no_output_____first_sweep_sensitive_feature_weights_dp = [
            predictor.coef_[0][1] for predictor in first_sweep_dp._predictors]
# plt.scatter(actual_multipliers_dp, first_sweep_sensitive_feature_weights_dp)
# plt.xlabel("Lagrange Multiplier for GS with Demographic Parity")
# plt.ylabel("Weight of Protected Attribute in Model")
# plt.show()_____no_output_____lambda_best_dp = first_sweep_dp._lambda_vecs[first_sweep_dp._best_grid_index][("+", "all", 1)] - first_sweep_dp._lambda_vecs[first_sweep_dp._best_grid_index][("-", "all", 1)]
# print("lambda_best_dp =", lambda_best_dp)
# print("coefficients_dp =", first_sweep_dp._predictors[first_sweep_dp._best_grid_index].coef_)_____no_output_____

#### Using Equalized Odds_____no_output_____

first_sweep_eo =  GridSearch(LogisticRegression(solver='liblinear', fit_intercept=True),
                   constraints=EqualizedOdds(),
                   grid_size=50,
                   grid_limit=10)
first_sweep_eo.fit(X_train, y_train_bool, sensitive_features=A_train)
Y_first_predict_eo = first_sweep_eo.predict(X_test)
best_results_eo_proba = first_sweep_eo.predict_proba(X_test)
scores_best_eo = pd.Series(best_results_eo_proba[:,1], name="score_best_model")_____no_output_____acc_gs_eo = group_metric_as_df("accuracy_gs_eo",
                                     group_accuracy_score(y_test_bool, Y_first_predict_eo, A_test_renamed))_____no_output_____acc_agg_gs = pd.concat([acc_unmitigated, acc_gs_dp, acc_gs_eo], axis=1)
display(acc_agg_gs)_____no_output_____

#### Mitigating Demographic Disparity with Exponentiated Gradient _____no_output_____ExponentiatedGradient also seeks to find a classifier that optimizes accuracy while placing a constraint on the demographic disparity. It operates as a reduction to standard classification, taking any estimator as a black box. During its run it repeatedly re-fits the estimator on variously reweighted training data and eventually produces a randomized classifier of the same type as the provided black-box estimator. This means that if the black box does not have access to the sensitive feature, neither will the predictor fitted by ExponentiatedGradient._____no_output_____
We train two classifiers via ExponentiatedGradient. Both use LogisticRegression as a black box. However, one has only access to the original features (X_train and X_test), whereas the other one also has access to the sensitive features, which we include in the extended feature set (XA_train and XA_test). Both classifiers optimize accuracy subject to the constraint that demographic disparity on training data is at most 0.01. We also set the convergence parameter nu to 1e-6 to optimize to numerical precision (the default is to optimize to statistical precision, which we override here)._____no_output_____

balanced_index_pass0 = y_train_bool[y_train==0].index
balanced_index_pass1 = y_train_bool[y_train_bool==1].sample(n=balanced_index_pass0.size, random_state=0).index
balanced_index = balanced_index_pass0.union(balanced_index_pass1)_____no_output_____XA_train = pd.concat([X_train, A_train==0], axis=1).astype(float)
XA_test = pd.concat([X_test, A_test==0], axis=1).astype(float)_____no_output_____

##### Using Demographic Parity as Fairness Constraint

_____no_output_____

expgrad_dp_X= ExponentiatedGradient(
    LogisticRegression(solver='liblinear', fit_intercept=True),
    constraints=DemographicParity(),
    eps=0.01,
    nu=1e-6)
expgrad_dp_XA = ExponentiatedGradient(
    LogisticRegression(solver='liblinear', fit_intercept=True),
    constraints=DemographicParity(),
    eps=0.01,
    nu=1e-6)

expgrad_dp_X.fit(
    X_train.iloc[balanced_index,:],
    y_train.iloc[balanced_index],
    sensitive_features=A_train.iloc[balanced_index])
expgrad_dp_XA.fit(
    XA_train.iloc[balanced_index,:],
    y_train.iloc[balanced_index],
    sensitive_features=A_train.iloc[balanced_index])_____no_output_____scores_expgrad_dp_X = pd.Series(expgrad_dp_X.predict(X_test), name="scores_dp_expgrad_X")
scores_expgrad_dp_XA = pd.Series(expgrad_dp_XA.predict(XA_test), name="scores_dp_expgrad_XA")

acc_expgrad_dp_X = group_metric_as_df(
    "accuracy_expgrad_dp_X",
    group_roc_auc_score(y_test_bool, scores_expgrad_dp_X, A_test_renamed))
acc_expgrad_dp_XA = group_metric_as_df(
    "accuracy_expgrad_dp_XA",
    group_roc_auc_score(y_test_bool, scores_expgrad_dp_XA, A_test_renamed))_____no_output_____

#### Using Equalized Odds_____no_output_____

expgrad_eo_X= ExponentiatedGradient(
    LogisticRegression(solver='liblinear', fit_intercept=True),
    constraints=EqualizedOdds(),
    eps=0.01,
    nu=1e-6)
expgrad_eo_XA = ExponentiatedGradient(
    LogisticRegression(solver='liblinear', fit_intercept=True),
    constraints=EqualizedOdds(),
    eps=0.01,
    nu=1e-6)

expgrad_eo_X.fit(
    X_train.iloc[balanced_index,:],
    y_train.iloc[balanced_index],
    sensitive_features=A_train.iloc[balanced_index])
expgrad_eo_XA.fit(
    XA_train.iloc[balanced_index,:],
    y_train.iloc[balanced_index],
    sensitive_features=A_train.iloc[balanced_index])_____no_output_____scores_expgrad_eo_X = pd.Series(expgrad_eo_X.predict(X_test), name="scores_eo_expgrad_X")
scores_expgrad_eo_XA = pd.Series(expgrad_eo_XA.predict(XA_test), name="scores_eo_expgrad_XA")

acc_expgrad_eo_X = group_metric_as_df(
    "accuracy_expgrad_eo_X",
    group_roc_auc_score(y_test_bool, scores_expgrad_eo_X, A_test_renamed))
acc_expgrad_eo_XA = group_metric_as_df(
    "accuracy_expgrad_eo_XA",
    group_roc_auc_score(y_test_bool, scores_expgrad_eo_XA, A_test_renamed))_____no_output_____acc_agg_expgrad = pd.concat([acc_unmitigated, acc_expgrad_dp_X, acc_expgrad_dp_XA, acc_expgrad_eo_X, acc_expgrad_eo_XA], axis=1)_____no_output_____display(acc_agg_expgrad)_____no_output_____

# BGL Regression_____no_output_____We apply bounded group loss to linear regression using Grid Search as defined in ["Fair Regression: Quantitative Definitions and Reduction-based Algorithms" (Agarwal et al. 2019)](https://arxiv.org/abs/1905.12843).

Bounded group loss requires that a predictor's loss remains below a specified level for each protected group. BGL minimizes the overall loss, while controlling the loss on a protected group (max-min fairness).

It is important to note BGL does not enforce an artificial decrease in performance to match the hardest to predict.

We apply BGL on the exam data with gender as our protected attribute. We saw in unmitigated linear regression that our prediction over-predicted male GPA and under-predicted female. _____no_output_____

bgl_square_loss = GroupLossMoment(SquareLoss(-np.inf, np.inf))
grid_search = GridSearch(LinearRegression(),
                             constraints=bgl_square_loss,
                             grid_size=41)
grid_search.fit(X_train, y_train, sensitive_features=A_train)
best_predict = grid_search.predict(X_test)_____no_output_____lambda_vecs_regression = grid_search._lambda_vecs
# print(lambda_vecs_regression)
actual_multipliers_regression = lambda_vecs_regression.loc[1]
actual_multipliers_regression_female = lambda_vecs_regression.loc[0]
print(grid_search._best_grid_index)

losses = [loss_fct(i) for i in range(len(grid_search._objectives))]
30

We run grid search using Bounded Group loss. We see that the best grid index occurs when the lagrange multiplier value of 1.5 for males and 0.5 for females.  _____no_output_____

mse_bgl = group_metric_as_df(
      "MSE BGL",
      group_mean_squared_error(y_test, best_predict, A_test))
mse_lr = group_metric_as_df(
      "MSE LR",
      group_mean_squared_error(y_test, lr_pred, A_test))
disp_bgl = group_metric_as_df(
    "Disparity BGL",
    group_mean_prediction(y_test, best_predict, A_test))
disp_lr = group_metric_as_df(
    "Disparity LR",
    group_mean_prediction(y_test, lr_pred, A_test))

over_pred_bgl = group_metric_as_df(
    "overpred BGL",
    group_mean_overprediction(y_test, best_predict, A_test))
over_pred_unmit = group_metric_as_df(
    "overpred LR",
    group_mean_overprediction(y_test, lr_pred, A_test))

bgl_summary_regression = pd.concat([mse_lr, mse_bgl, disp_lr, disp_bgl, over_pred_unmit, over_pred_bgl], axis=1)
bgl_summary_regression.loc['disparity']=(bgl_summary_regression.loc[0]-bgl_summary_regression.loc[1]).abs()
bgl_summary_regression.loc['disparity', bgl_summary_regression.columns.str.startswith('mse')]='-'
bgl_summary_regression.loc['disparity', bgl_summary_regression.columns.str.startswith('overpred')]='-'
bgl_summary_regression.rename(index={0: "female", 1: "male"}, inplace=True)
display(bgl_summary_regression)_____no_output_____

We see that the MSE of the BGL regression is higher for both male and female compared to unmitigated regression. We see that the disparity between the two groups is smaller in BGL.

We do see that the overprediction of males decreases in BGL. This is good as we saw that our unmitigated regressor overpredicted male GPA. _____no_output_____

y_preds_bgl = [grid_search._predictors[i].predict(X_test) for i in range(len(grid_search._predictors))]

mse_bgl_groups = [group_metric_as_df(i, group_mean_squared_error(y_test, y_preds_bgl[i], A_test)) for i in range(len(y_preds_bgl))]
disp_bgl_groups = [group_metric_as_df(i, group_mean_prediction(y_test, y_preds_bgl[i], A_test)) for i in range(len(y_preds_bgl))]
overpred_bgl_groups = [group_metric_as_df(i, group_mean_overprediction(y_test, y_preds_bgl[i], A_test)) for i in range(len(y_preds_bgl))]

bgl_df_mse = pd.concat(mse_bgl_groups, axis = 1)
bgl_df_disp = pd.concat(disp_bgl_groups, axis = 1)
bgl_df_overpred = pd.concat(overpred_bgl_groups, axis = 1)

plt.scatter(bgl_df_disp.loc['overall'], bgl_df_mse.loc['overall'], c = 'lightblue', label='Grid Search')
plt.scatter(disp_lr.loc['overall'], mse_lr.loc['overall'], c = 'coral', label = ' Unmitigated Linear Regression')
plt.scatter(disp_bgl.loc['overall'], mse_bgl.loc['overall'], c = 'green', label = 'Best Grid Search Predictor')
plt.xlabel("Overall Disparity")
plt.ylabel("MSE")
plt.legend()
plt.show()

plt.scatter(bgl_df_disp.loc[0], bgl_df_mse.loc[0], c = 'lightblue', label='Grid Search')
plt.scatter(disp_lr.loc[0], mse_lr.loc[0], c = 'coral', label = ' Unmitigated Linear Regression')
plt.scatter(disp_bgl.loc[0], mse_bgl.loc[0], c = 'green', label = 'Best Grid Search Predictor')
plt.xlabel("Disparity Female")
plt.ylabel("MSE Female")
plt.legend()
plt.show()

plt.scatter(bgl_df_disp.loc[1], bgl_df_mse.loc[1], c = 'lightblue', label='Grid Search')
plt.scatter(disp_lr.loc[1], mse_lr.loc[1], c = 'coral', label = ' Unmitigated Linear Regression')
plt.scatter(disp_bgl.loc[1], mse_bgl.loc[1], c = 'green', label = 'Best Grid Search Predictor')
plt.xlabel("Disparity Male")
plt.ylabel("MSE Male")
plt.legend()
plt.show()

_____no_output_____

We see that the MSE is smallest on the unmitigated linear regression. However, BGL in Grid Search selects a model that has a slightly higher MSE. However, the overall disparity between male and female is lower in the chosen model.

With BGL we want to minimize the overall loss on the model while ensuring each protected groups' loss is bounded within a certain level.

_____no_output_____

plt.scatter(bgl_df_disp.loc['overall'], bgl_df_overpred.loc['overall'], c = 'lightblue', label='Grid Search')
plt.scatter(disp_lr.loc['overall'], over_pred_unmit.loc['overall'], c = 'coral', label = ' Unmitigated Linear Regression')
plt.scatter(disp_bgl.loc['overall'], over_pred_bgl.loc['overall'], c = 'green', label = 'Best Grid Search Predictor')
plt.xlabel("Overall Disparity")
plt.ylabel("OverPrediction")
plt.legend()
plt.show()

plt.scatter(bgl_df_disp.loc[0], bgl_df_overpred.loc[0], c = 'lightblue', label='Grid Search')
plt.scatter(disp_lr.loc[0], over_pred_unmit.loc[0], c = 'coral', label = ' Unmitigated Linear Regression')
plt.scatter(disp_bgl.loc[0], over_pred_bgl.loc[0], c = 'green', label = 'Best Grid Search Predictor')
plt.xlabel("Disparity Female")
plt.ylabel("Over Prediction Female")
plt.legend()
plt.show()

plt.scatter(bgl_df_disp.loc[1], bgl_df_overpred.loc[1], c = 'lightblue', label='Grid Search')
plt.scatter(disp_lr.loc[1], over_pred_unmit.loc[1], c = 'coral', label = ' Unmitigated Linear Regression')
plt.scatter(disp_bgl.loc[1], over_pred_bgl.loc[1], c = 'green', label = 'Best Grid Search Predictor')
plt.xlabel("Disparity Male")
plt.ylabel("Over Prediction Male")
plt.legend()
plt.show()_____no_output_____

Furthermore, BGL selects a model that minimizes both the overall disparity as well as the overprediction rate. In the unmitigated linear regression, we saw that male students were over-predicted and female students were under-predicted. BGL lowers the overprediction for both male and female as well as the disparity between the two. However, since females naturally have a higher GPA we are still undercounting female performance in this new model. _____no_output_____# Future Work _____no_output_____In the next half of the quarter, we are going to work on implementing BGL for Exponentiated Gradient. To do so requires some re-configuring of the current code base.

**Incorporating Right Hand Side to Gamma**

Most of the code was written with the original goal of classification. As we move towards regression, we need to make some structural changes. In classification, we have two parameters self.gamma and self.eps within the lagrangian. The gamma comes from the [moments](https://github.com/fairlearn/fairlearn/tree/master/fairlearn/reductions/_moments) classes in the repo. Our first task is to incorporate epsilon into the moments class for the different moments available. We will create a new parameter in the moments and a new method bound. Depending on the moment, the bound will be created by parameters passed into the constructor for the moment. The bound method will return a vector of epsilon values representing the bound. When we call the oracle we will be calling

   *L = error + np.sum(lambda_vec * (gamma - bound))*

 as opposed to

  *L = error + np.sum(lambda_vec * (gamma - self.eps))*

**Adding Control Flow to Lagrangian to Allow for Classification and Regression**

After incorporating the right hand side of gamma into the moments, we can update the lagrangian. BGL in exponentiated gradient uses a different call to the oracle than classification. Whereas the currently implemented use cases implement a cost-senstivive classification, BGL frames the problem as a risk minimization optimization problem. We need to update the current lagrangian to handle both regression and classification. The main changes will be within the [_call_oracle ](https://github.com/fairlearn/fairlearn/blob/master/fairlearn/reductions/_exponentiated_gradient/_lagrangian.py)method. Unlike in classification, we do not update the classes of Y in our optimization. We will add a control flow to the lagrangian to detect classification vs. regression reductions and apply correct oracle to solve the optimization.

**Testing Implementation works as desired**

As we make these changes we not only need to test that the new code is compatible with the current infrastructure, but we also need to test that the BGL in Exponentiated Gradient works as desired. We will thoroughly test our implementation to ensure it works correctly.
_____no_output_____# Further Clarifications _____no_output_____