<jupyter_start><jupyter_text>Copyright (c) Microsoft Corporation. All rights reserved.  Licensed under the MIT License. ![Impressions](https://PixelServer20190423114238.azurewebsites.net/api/impressions/MachineLearningNotebooks/contrib/fairness/fairlearn-azureml-mitigation.png)  Unfairness Mitigation with Fairlearn and Azure Machine Learning**This notebook shows how to upload results from Fairlearn's GridSearch mitigation algorithm into a dashboard in Azure Machine Learning Studio** Table of Contents1. [Introduction](Introduction)1. [Loading the Data](LoadingData)1. [Training an Unmitigated Model](UnmitigatedModel)1. [Mitigation with GridSearch](Mitigation)1. [Uploading a Fairness Dashboard to Azure](AzureUpload)    1. Registering models    1. Computing Fairness Metrics    1. Uploading to Azure1. [Conclusion](Conclusion) IntroductionThis notebook shows how to use [Fairlearn (an open source fairness assessment and unfairness mitigation package)](http://fairlearn.github.io) and Azure Machine Learning Studio for a binary classification problem. This example uses the well-known adult census dataset. For the purposes of this notebook, we shall treat this as a loan decision problem. We will pretend that the label indicates whether or not each individual repaid a loan in the past. We will use the data to train a predictor to predict whether previously unseen individuals will repay a loan or not. The assumption is that the model predictions are used to decide whether an individual should be offered a loan. Its purpose is purely illustrative of a workflow including a fairness dashboard - in particular, we do **not** include a full discussion of the detailed issues which arise when considering fairness in machine learning. For such discussions, please [refer to the Fairlearn website](http://fairlearn.github.io/).We will apply the [grid search algorithm](https://fairlearn.github.io/master/api_reference/fairlearn.reductions.htmlfairlearn.reductions.GridSearch) from the Fairlearn package using a specific notion of fairness called Demographic Parity. This produces a set of models, and we will view these in a dashboard both locally and in the Azure Machine Learning Studio. SetupTo use this notebook, an Azure Machine Learning workspace is required.Please see the [configuration notebook](../../configuration.ipynb) for information about creating one, if required.This notebook also requires the following packages:* `azureml-contrib-fairness`* `fairlearn==0.4.6`* `joblib`* `shap`Fairlearn relies on features introduced in v0.22.1 of `scikit-learn`. If you have an older version already installed, please uncomment and run the following cell:<jupyter_code># !pip install --upgrade scikit-learn>=0.22.1<jupyter_output><empty_output><jupyter_text>Loading the DataWe use the well-known `adult` census dataset, which we load using `shap` (for convenience). We start with a fairly unremarkable set of imports:<jupyter_code>from fairlearn.reductions import GridSearch, DemographicParity, ErrorRate
from fairlearn.widget import FairlearnDashboard
from sklearn import svm
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.linear_model import LogisticRegression
import pandas as pd<jupyter_output><empty_output><jupyter_text>We can now load and inspect the data from the `shap` package:<jupyter_code>from utilities import fetch_openml_with_retries

data = fetch_openml_with_retries(data_id=1590)
    
# Extract the items we want
X_raw = data.data
Y = (data.target == '>50K') * 1

X_raw["race"].value_counts().to_dict()<jupyter_output><empty_output><jupyter_text>We are going to treat the sex of each individual as a protected attribute (where 0 indicates female and 1 indicates male), and in this particular case we are going separate this attribute out and drop it from the main data (this is not always the best option - see the [Fairlearn website](http://fairlearn.github.io/) for further discussion). We also separate out the Race column, but we will not perform any mitigation based on it. Finally, we perform some standard data preprocessing steps to convert the data into a format suitable for the ML algorithms<jupyter_code>A = X_raw[['sex','race']]
X = X_raw.drop(labels=['sex', 'race'],axis = 1)
X_dummies = pd.get_dummies(X)

sc = StandardScaler()
X_scaled = sc.fit_transform(X_dummies)
X_scaled = pd.DataFrame(X_scaled, columns=X_dummies.columns)


le = LabelEncoder()
Y = le.fit_transform(Y)<jupyter_output><empty_output><jupyter_text>With our data prepared, we can make the conventional split in to 'test' and 'train' subsets:<jupyter_code>from sklearn.model_selection import train_test_split
X_train, X_test, Y_train, Y_test, A_train, A_test = train_test_split(X_scaled, 
                                                    Y, 
                                                    A,
                                                    test_size = 0.2,
                                                    random_state=0,
                                                    stratify=Y)

# Work around indexing issue
X_train = X_train.reset_index(drop=True)
A_train = A_train.reset_index(drop=True)
X_test = X_test.reset_index(drop=True)
A_test = A_test.reset_index(drop=True)<jupyter_output><empty_output><jupyter_text>Training an Unmitigated ModelSo we have a point of comparison, we first train a model (specifically, logistic regression from scikit-learn) on the raw data, without applying any mitigation algorithm:<jupyter_code>unmitigated_predictor = LogisticRegression(solver='liblinear', fit_intercept=True)

unmitigated_predictor.fit(X_train, Y_train)<jupyter_output><empty_output><jupyter_text>We can view this model in the fairness dashboard, and see the disparities which appear:<jupyter_code>FairlearnDashboard(sensitive_features=A_test, sensitive_feature_names=['Sex', 'Race'],
                   y_true=Y_test,
                   y_pred={"unmitigated": unmitigated_predictor.predict(X_test)})<jupyter_output><empty_output><jupyter_text>Looking at the disparity in accuracy when we select 'Sex' as the sensitive feature, we see that males have an error rate about three times greater than the females. More interesting is the disparity in opportunitiy - males are offered loans at three times the rate of females.Despite the fact that we removed the feature from the training data, our predictor still discriminates based on sex. This demonstrates that simply ignoring a protected attribute when fitting a predictor rarely eliminates unfairness. There will generally be enough other features correlated with the removed attribute to lead to disparate impact.  Mitigation with GridSearchThe `GridSearch` class in `Fairlearn` implements a simplified version of the exponentiated gradient reduction of [Agarwal et al. 2018](https://arxiv.org/abs/1803.02453). The user supplies a standard ML estimator, which is treated as a blackbox - for this simple example, we shall use the logistic regression estimator from scikit-learn. `GridSearch` works by generating a sequence of relabellings and reweightings, and trains a predictor for each.For this example, we specify demographic parity (on the protected attribute of sex) as the fairness metric. Demographic parity requires that individuals are offered the opportunity (a loan in this example) independent of membership in the protected class (i.e., females and males should be offered loans at the same rate). *We are using this metric for the sake of simplicity* in this example; the appropriate fairness metric can only be selected after *careful examination of the broader context* in which the model is to be used.<jupyter_code>sweep = GridSearch(LogisticRegression(solver='liblinear', fit_intercept=True),
                   constraints=DemographicParity(),
                   grid_size=71)<jupyter_output><empty_output><jupyter_text>With our estimator created, we can fit it to the data. After `fit()` completes, we extract the full set of predictors from the `GridSearch` object.The following cell trains a many copies of the underlying estimator, and may take a minute or two to run:<jupyter_code>sweep.fit(X_train, Y_train,
          sensitive_features=A_train.sex)

predictors = sweep._predictors<jupyter_output><empty_output><jupyter_text>We could load these predictors into the Fairness dashboard now. However, the plot would be somewhat confusing due to their number. In this case, we are going to remove the predictors which are dominated in the error-disparity space by others from the sweep (note that the disparity will only be calculated for the protected attribute; other potentially protected attributes will *not* be mitigated). In general, one might not want to do this, since there may be other considerations beyond the strict optimisation of error and disparity (of the given protected attribute).<jupyter_code>errors, disparities = [], []
for m in predictors:
    classifier = lambda X: m.predict(X)
    
    error = ErrorRate()
    error.load_data(X_train, pd.Series(Y_train), sensitive_features=A_train.sex)
    disparity = DemographicParity()
    disparity.load_data(X_train, pd.Series(Y_train), sensitive_features=A_train.sex)
    
    errors.append(error.gamma(classifier)[0])
    disparities.append(disparity.gamma(classifier).max())
    
all_results = pd.DataFrame( {"predictor": predictors, "error": errors, "disparity": disparities})

dominant_models_dict = dict()
base_name_format = "census_gs_model_{0}"
row_id = 0
for row in all_results.itertuples():
    model_name = base_name_format.format(row_id)
    errors_for_lower_or_eq_disparity = all_results["error"][all_results["disparity"]<=row.disparity]
    if row.error <= errors_for_lower_or_eq_disparity.min():
        dominant_models_dict[model_name] = row.predictor
    row_id = row_id + 1<jupyter_output><empty_output><jupyter_text>We can construct predictions for the dominant models (we include the unmitigated predictor as well, for comparison):<jupyter_code>predictions_dominant = {"census_unmitigated": unmitigated_predictor.predict(X_test)}
models_dominant = {"census_unmitigated": unmitigated_predictor}
for name, predictor in dominant_models_dict.items():
    value = predictor.predict(X_test)
    predictions_dominant[name] = value
    models_dominant[name] = predictor<jupyter_output><empty_output><jupyter_text>These predictions may then be viewed in the fairness dashboard. We include the race column from the dataset, as an alternative basis for assessing the models. However, since we have not based our mitigation on it, the variation in the models with respect to race can be large.<jupyter_code>FairlearnDashboard(sensitive_features=A_test, 
                   sensitive_feature_names=['Sex', 'Race'],
                   y_true=Y_test.tolist(),
                   y_pred=predictions_dominant)<jupyter_output><empty_output><jupyter_text>When using sex as the sensitive feature, we see a Pareto front forming - the set of predictors which represent optimal tradeoffs between accuracy and disparity in predictions. In the ideal case, we would have a predictor at (1,0) - perfectly accurate and without any unfairness under demographic parity (with respect to the protected attribute "sex"). The Pareto front represents the closest we can come to this ideal based on our data and choice of estimator. Note the range of the axes - the disparity axis covers more values than the accuracy, so we can reduce disparity substantially for a small loss in accuracy. Finally, we also see that the unmitigated model is towards the top right of the plot, with high accuracy, but worst disparity.By clicking on individual models on the plot, we can inspect their metrics for disparity and accuracy in greater detail. In a real example, we would then pick the model which represented the best trade-off between accuracy and disparity given the relevant business constraints.  Uploading a Fairness Dashboard to AzureUploading a fairness dashboard to Azure is a two stage process. The `FairlearnDashboard` invoked in the previous section relies on the underlying Python kernel to compute metrics on demand. This is obviously not available when the fairness dashboard is rendered in AzureML Studio. By default, the dashboard in Azure Machine Learning Studio also requires the models to be registered. The required stages are therefore:1. Register the dominant models1. Precompute all the required metrics1. Upload to AzureBefore that, we need to connect to Azure Machine Learning Studio:<jupyter_code>from azureml.core import Workspace, Experiment, Model

ws = Workspace.from_config()
ws.get_details()<jupyter_output><empty_output><jupyter_text>Registering ModelsThe fairness dashboard is designed to integrate with registered models, so we need to do this for the models we want in the Studio portal. The assumption is that the names of the models specified in the dashboard dictionary correspond to the `id`s (i.e. `:` pairs) of registered models in the workspace. We register each of the models in the `models_dominant` dictionary into the workspace. For this, we have to save each model to a file, and then register that file:<jupyter_code>import joblib
import os

os.makedirs('models', exist_ok=True)
def register_model(name, model):
    print("Registering ", name)
    model_path = "models/{0}.pkl".format(name)
    joblib.dump(value=model, filename=model_path)
    registered_model = Model.register(model_path=model_path,
                                      model_name=name,
                                      workspace=ws)
    print("Registered ", registered_model.id)
    return registered_model.id

model_name_id_mapping = dict()
for name, model in models_dominant.items():
    m_id = register_model(name, model)
    model_name_id_mapping[name] = m_id<jupyter_output><empty_output><jupyter_text>Now, produce new predictions dictionaries, with the updated names:<jupyter_code>predictions_dominant_ids = dict()
for name, y_pred in predictions_dominant.items():
    predictions_dominant_ids[model_name_id_mapping[name]] = y_pred<jupyter_output><empty_output><jupyter_text>Precomputing MetricsWe create a _dashboard dictionary_ using Fairlearn's `metrics` package. The `_create_group_metric_set` method has arguments similar to the Dashboard constructor, except that the sensitive features are passed as a dictionary (to ensure that names are available), and we must specify the type of prediction. Note that we use the `predictions_dominant_ids` dictionary we just created:<jupyter_code>sf = { 'sex': A_test.sex, 'race': A_test.race }

from fairlearn.metrics._group_metric_set import _create_group_metric_set


dash_dict = _create_group_metric_set(y_true=Y_test,
                                     predictions=predictions_dominant_ids,
                                     sensitive_features=sf,
                                     prediction_type='binary_classification')<jupyter_output><empty_output><jupyter_text>Uploading the DashboardNow, we import our `contrib` package which contains the routine to perform the upload:<jupyter_code>from azureml.contrib.fairness import upload_dashboard_dictionary, download_dashboard_by_upload_id<jupyter_output><empty_output><jupyter_text>Now we can create an Experiment, then a Run, and upload our dashboard to it:<jupyter_code>exp = Experiment(ws, "Test_Fairlearn_GridSearch_Census_Demo")
print(exp)

run = exp.start_logging()
try:
    dashboard_title = "Dominant Models from GridSearch"
    upload_id = upload_dashboard_dictionary(run,
                                            dash_dict,
                                            dashboard_name=dashboard_title)
    print("\nUploaded to id: {0}\n".format(upload_id))

    downloaded_dict = download_dashboard_by_upload_id(run, upload_id)
finally:
    run.complete()<jupyter_output><empty_output><jupyter_text>The dashboard can be viewed in the Run Details page.Finally, we can verify that the dashboard dictionary which we downloaded matches our upload:<jupyter_code>print(dash_dict == downloaded_dict)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.06593132672773727, 'fraction_numerical': 0.004549136910325918, 'mean_word_length': 4.715099715099715, 'pattern_counts': {'":': 6, '<': 79, '<?xml version=': 0, '>': 79, 'https://': 3, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 26, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '19', 'dir': 'jupyter-structured-clean-dedup', 'id': '241609', 'license': 'MIT', 'n_tokens_mistral': 4376, 'n_tokens_neox': 4164, 'n_words': 1662, 'path': 'contrib/fairness/fairlearn-azureml-mitigation.ipynb', 'repo_name': 'zronaghi/MachineLearningNotebooks', 'size': '16047'}