In this tutorial, we explore how algorithmic fairness can be quantified and explored, and what one may need to pay attention to. We do this through the FairBench library, which is tailored to practical use in many pipelines (regardless of whether they focus on numpy, pandas, torch,tf, etc.). Fairbench serves as a comprehensive AI fairness exploration framework, offering tools to:
- Build measures from simpler blocks
- Create fairness reports and stamps
- Perform multivalue multiattribute analysis
- Backtrack, filter, and reorganize computations
- Integrate with your workflow: numpy, pandas, torch, tensorflow, jax
Measuring algorithmic fairness¶
In this tutorial we explore how algorithmic fairness can be quantified and what one may need to pay attention to. We do this through the FairBench library, which is tailored to practical use in many pipelines (regardless of whether they focus on numpy,pandas,torch,tf, etc.).
We will start by installing the library from PyPi. The main library is lightweight and you can even test it through its website, but there are some installation extras if you want to run out-of-the-box experiments with images or LLMs. The snippet below throttles printing to keep this tutorial concise.
!pip install fairbench > /dev/null
Some data¶
First, load the library and get some data that are ready for experimentation. FairBench bootstraps the learning experience with several benchmakrs from the tabular, graph, vision, and text (LLM) data domains. You can later substitute your own algorithms and run fairness analaysis with the exact same interfaces.
As a first example, let us load the popular (in fairness analysis circles) and highly contentious COMPAS dataset. This is one of the more high-profile cases that helped bring attention to algorithmic fairness. We get the library to download, train a lightweight model (logistic regression with a handwritten gradient descent implementation) and export some predictions for the datasets. Let's look at some loaded test data characteristics, namely whether a criminal recidivised within two years y, what was the prediction yhat, and training columns x out of which x["sex"] and x["race"] could be sensitive attributes.
import fairbench as fb
x, y, yhat = fb.bench.tabular.compas(test_size=0.5)
print(y)
print(yhat)
print(x["sex"])
print(x["race"])
This particular benchmark uses only builtin functionalities from FairBench, but you may just as easily run a similar analysis on data of your own. You just need your data to be numerical or categorical iterables, even lists like pandas columns, arrays, lists, etc. Here is an example with lists, though we will not use it.
y = [0,1,0,1,0,1]
yhat = [0,0,1,1,0,0]
sensitive = ["M","F","NB","F,"M"]
Before moving on to actual computations, we will pack our sensitive attributes in a standardized format. Mainly, we want to organize potentially more than one attributes, each of which may span different values, into sensitive dimensions. Below is a code for doing so. The @ operator applies a chosen unpacking mechanism (in this case: each category into a separate dimension).
Under the hood, each of the unpacked dimensions is an array that has 1 at the positions where the corresponding data sample belongs to the corresponding demographic group.
sensitive = fb.Dimensions(fb.categories@x["sex"], fb.categories@x["race"])
sensitive
Oftentimes, we need to recognize all possible intersections. This is done like below, where we also prevent the existence of broader categories if at least one strict subset exists with strict. The minimum number of elements in each retained intersection is also set - the default is 1 but we set a higher number to ensure analysis of only adequately represented groups. Notice that dimensions like "Male&Native American" are ignored in our case, which could be worrysome.
sensitive = fb.Dimensions(fb.categories@x["sex"], fb.categories@x["race"])
sensitive = sensitive.intersectional(min_size=10).strict()
sensitive
EXERCISE 1¶
- Reduce
min_sizeto 1. What are the risks in doing so? Usesensitive.sum()to count each dimension's elements. - How would you feel if you belonged to a group intersection not included in analysis?
- Would you be ok with removing the
strictpart of intersections?
There is no "correct" answer to the above in a vaccum, just state your opinion. We haven't even bagun tackling fairness yet and we have hard problems to think about.
# EXPERIMENT HERE
Quickly compute a measure¶
As a first task, let us quickly compute some measures. FairBench does not promote blindly writing down definitions of fairness, because there are only so many options that can be named. Instead, it constructs measures by combining them from simpler building blocks.
Nonetheless, before applying the full process, compute a measure across all samples.
Measures accept only keyword arguments.
float(fb.measures.acc(predictions=yhat,labels=y))
The conversion to float was needed because FairBench computations result in complex values that are traceable to intermediate computations. For example, you can see all surface-level information like below. The default visualization mechanism is ANSI console colors but more exist.
acc = fb.measures.acc(predictions=yhat,labels=y)
acc.show()
acc.help()
Now let us also compute the measure for each sensitive attribute dimension, and get the minimum across all of those. Operations are all applied at the same time. This measure is dynamically built from its name (more than 300 ones can be created), which consists of underscore-separated options of what to compute, how to aggregate values, and -if needed- whether groups are compared pairwise or against the total population.
fb.quick.maxdiff_acc_pairwise(predictions=yhat,labels=y,sensitive=sensitive).show(depth=0)
EXERCISE 2¶
Starting from the above snippet:
- Increase the shown depth to increase the level of detail.
- Get the actual positives (ap) for each group. Are there unexpected imbalances there?
- Compute the maximum accuracy difference (
maxdiff) between groups. - Compute the the maximum true positive rate (
tpr) and true negative rate (tnr) between groups. Are there notworthy imbalances?
# EXPERIMENT HERE
Thorough investigation with reports¶
In the above excercise, you were called to manually investigate different combinations of base measures and means of aggregating their assessment to one value. The combinations are many, each catering to a different kind of sensibility. Even absolute instead of relative differences may play a huge role on how one views fairness or bias.
The question, of course, is why not look at raw values at that point. However, as group intersections grow in number, it becomes harder to gain an high-level view of actual bias. Instead, FairBench proposes looking at a set of standardized measures that have clear-cut interpretations, finding potentially problematic values, and then backtrack those values to actual algorithmic issues.
Always consult with relevant stakeholders once you identify issues.
Several assessments are packed by the library into reports, like the one below. Those reports may contain too much information to comfortably parse at a glance, so you may want to switch to a different visualization evnironment. Here we will use ConsoleTable, which condenses information as much as possible. The coloring is only a visual aid of which values to noice first, but the actual problems may be dressed in green.
Not everything matters, but seeing many things does. You will always have some bad metrics (see: impossibility theorems).
report = fb.reports.pairwise(
sensitive=sensitive,
predictions=yhat,
labels=y,
targets=y,
)
report.show(env=fb.export.ConsoleTable)
So far, we have only shown console visualizations of reports, but we can also export some HTML. If you are working in a non-Jupyter environment, you can just change the visualization engine and a page will open in your browser. But here we can just manually get the text and show it. Below we do so while also focusing one one row (could also focus on a column) of the table with the dot notation.
from IPython.display import display, HTML
display(HTML(report.acc.show(env=fb.export.Html(view=False, filename=None), depth=1)))
All these values may be complicated to show, so FairBench also lets one filter which values should be shown / change the colorization threshold. Filtering can also make more drastic adjustments. Below is an example to helps us get a sense of -at the surface- most pressing biases issues by only keeping measures of bias (ideally fair value = 0) and only the values that differ by more than 0.2 with their ideal ones. Again, coloring or checkmarks when showing the full report will help you sub-divide the issues.
These are the starting issues.- in the end you will need to look at as much material as possible.
filtered_report = report.filter(fb.investigate.IsBias).filter(fb.investigate.DeviationsOver(0.2))
display(HTML(filtered_report.show(env=fb.export.HtmlTable(view=False, filename=None), depth=1)))
Starting from here, look at maxrel.tar to understand what it represents and where the imbalances lie: namely we have less true positives (in absolute numbers) for all other groups compared to Male&African-American, meaning that the system is better at identifying more of them correctly as recidivism (in absolute numbers, not relative numbers - tpr tells a different story in being more balanced and hence not shown).
There could be many socio-technical reasons for this difference, as indicated by the fact that we simply have more of those people, but they exhibit similar positive rates with other groups. So this issue should be discussed at length with relevant stakeholders in each scenario.
We do not run the full COMPAS analysis, which has been heavily criticized as biased, so DO NOT CONSIDER THESE NUMBERS AS ANY INDICATION OF WHAT HAPPENS IN REALITY.
filtered_report.maxrel.tar.show()
filtered_report.maxrel.tar.help()
filtered_report.maxrel.tar.samples.show()
filtered_report.maxrel.pr.show()
EXERCISE 3¶
COMPAS was a system that aims to not only predicted criminal recidivisim but also tried to provide some scores on whether each person would re-offend. That is, it was used as a recommendation system.
- Get a
yhatthat stores scores by addingpredict="probabilities"to the benchmark data loader in the beginning of this notebook. Rerun the data section. - Generate a fairness report that accounts for the recocommendations. To do this, just change
predictionstoscores. FairBench will automatically apply all relevant measures. - Explore the result and try to uncover problematic behavior.
# EXPERIMENT HERE
EXERCISE 4¶
Consider the threshold yhat>0.5 on the above scores to indicate a prediction threshold.
- Create a large report that contains both predictive and recommendation measures by having both
predictionsandscoresas arguments. - Apply
report = report.filter(fb.investigate.Stamps)to convert the report to a model card containing popular measures of bias/fairness - referred to as fairness stamps. Show the results a console table. Are you satisfied with the breadth of this exploration? - Show the model card fully and take note of caveats and recommendations personalized on those cards. Are stamps indications of fairness?
- Look at the
maxbareaatdepth=1. Are you ok with what you are seeing?
# EXPERIMENT HERE
EXERCISE 5¶
As a final step, look at the 0.75 threshold in the following code that considers only the education background when granting loan approvals and only a few base measures to look at.
- Adjust it to forcefully make min
tprlarge. How istnrand accuracy affected? - Adjust it to forcefully make min
tnrlarge. How istprand accuracy affected? - Use 0.9999 as a threshold and look at the distribution in
min.acc. All good? What happens tomin.pr? (Note: setdepth=2to just look at the distributions.)
import fairbench as fb
x, y, yhat = fb.bench.tabular.bank(test_size=0.5, predict="probabilities")
sensitive = fb.Dimensions(fb.categories@x["education"])
sensitive
report = fb.reports.pairwise(
sensitive=sensitive,
predictions=yhat>0.75,
labels=y,
targets=y,
measures=[fb.blocks.measures.acc, fb.blocks.measures.pr, fb.blocks.measures.tpr, fb.blocks.measures.tnr]
)
report.show(env=fb.export.ConsoleTable(sideways=True))