#!/usr/bin/env python
# coding: utf-8
# In[ ]:
get_ipython().run_line_magic('matplotlib', 'inline')
#
# # Measuring Fairness of a Data Set
#
#
# This example illustrates how to find unfair rows in a data set using the
# :func:`fatf.fairness.data.measures.systemic_bias` function and how to check
# whether each class is distributed equally between values of a selected feature,
# i.e. measuring the Sample Size Disparity (with :func:`fatf.utils.data.tools.group_by_column` function).
#
#
Note
Please note that this example uses a data set that is represented as a
# structured numpy array, which supports mixed data types among columns with
# the features (columns) being index by the feature name rather than by
# consecutive integers.
#
# In[ ]:
# Author: Kacper Sokol
# License: new BSD
from pprint import pprint
import numpy as np
import fatf.utils.data.datasets as fatf_datasets
import fatf.fairness.data.measures as fatf_dfm
import fatf.utils.data.tools as fatf_data_tools
print(__doc__)
# Load data
hr_data_dict = fatf_datasets.load_health_records()
hr_X = hr_data_dict['data']
hr_y = hr_data_dict['target']
hr_feature_names = hr_data_dict['feature_names']
hr_class_names = hr_data_dict['target_names']
# Systemic Bias
# -------------
#
# Before we proceed, we need to select which feature are **protected**, i.e.
# which ones are illegal to use when generating the prediction.
#
# We use them to see whether the data set contains rows that differ in some of
# the protected features and the labels (ground truth) but not in the rest of
# the features.
#
# The example presented below is rather naive as we do not have access to a
# more complicated dataset within the FAT Forensics package. To demonstrate the
# functionality of the we indicate all but one feature to be protected, hence
# we are guaranteed to find quite a few unfair rows in the health records data
# set. This means that "unfair" data rows are the ones that have the same value
# of the *diagnosis* feature (with rest of the feature values being
# unimportant) and differ in their target (ground truth) value.
#
# Systematic bias is expressed here as a square matrix (numpy array) of length
# equal to the number of rows in the data array. Each element of this matrix
# is a boolean indicating whether the rows in the data array with a particular
# pair of indices (the row and column indices of the boolean matrix) violate
# the aforementioned fairness criterion.
#
#
# In[ ]:
# Select which features should be treated as protected
protected_features = [
'name', 'email', 'age', 'weight', 'gender', 'zipcode', 'dob'
]
# Compute the data fairness matrix
data_fairness_matrix = fatf_dfm.systemic_bias(hr_X, hr_y, protected_features)
# Check if the data set is unfair (at least one unfair pair of data points)
is_data_unfair = fatf_dfm.systemic_bias_check(data_fairness_matrix)
# Identify which pairs of indices cause the unfairness
unfair_pairs_tuple = np.where(data_fairness_matrix)
unfair_pairs = []
for i, j in zip(*unfair_pairs_tuple):
pair_a, pair_b = (i, j), (j, i)
if pair_a not in unfair_pairs and pair_b not in unfair_pairs:
unfair_pairs.append(pair_a)
# Print out whether the fairness condition is violated
if is_data_unfair:
unfair_n = len(unfair_pairs)
unfair_fill = ('is', '') if unfair_n == 1 else ('are', 's')
print('\nThere {} {} pair{} of data points that violates the fairness '
'criterion.\n'.format(unfair_fill[0], unfair_n, unfair_fill[1]))
else:
print('The data set is fair.\n')
# Show the first pair of violating rows
pprint(hr_X[[unfair_pairs[0][0], unfair_pairs[0][1]]])
# Sample Size Disparity
# ---------------------
#
# The measure of Sample Size Disparity can be achieved by calling the
# :func:`fatf.utils.data.tools.group_by_column` grouping function and counting
# the number of instances in each group. By doing that for the *target vector*
# (ground truth) we can see whether the classes in our data set are balanced
# for each sub-group defined by a specified set of values for that feature.
#
# In the example below we will check whether there are roughly the same number
# of data points collected for *males* and *females*. Then we will see whether
# the class distribution (*fail* and *success*) for these two sub-populations
# is similar.
#
#
# In[ ]:
# Group the data based on the unique values of the 'gender' column
grouping_column = 'gender'
grouping_indices, grouping_names = fatf_data_tools.group_by_column(
hr_X, grouping_column, treat_as_categorical=True)
# Print out the data distribution for the grouping
print('The grouping based on the *{}* feature has the '
'following distribution:'.format(grouping_column))
for grouping_name, grouping_idx in zip(grouping_names, grouping_indices):
print(' * "{}" grouping has {} instances.'.format(
grouping_name, len(grouping_idx)))
# Get the class distribution for each sub-grouping
grouping_class_distribution = dict()
for grouping_name, grouping_idx in zip(grouping_names, grouping_indices):
sg_y = hr_y[grouping_idx]
sg_classes, sg_counts = np.unique(sg_y, return_counts=True)
grouping_class_distribution[grouping_name] = dict()
for sg_class, sg_count in zip(sg_classes, sg_counts):
sg_class_name = hr_class_names[sg_class]
grouping_class_distribution[grouping_name][sg_class_name] = sg_count
# Print out the class distribution per sub-group
print('\nThe class distribution per sub-population:')
for grouping_name, class_distribution in grouping_class_distribution.items():
print(' * For the "{}" grouping the classes are distributed as '
'follows:'.format(grouping_name))
for class_name, class_count in class_distribution.items():
print(' - The class *{}* has {} data points.'.format(
class_name, class_count))