Dimensions (creation patterns)
Dimensions is a flexible data structures that stores
multidimensional values, such as
sensitive attributes or predictions and labels for
multiple classes. Each dimension holds its own
vector of values.
For example, you might have a
dimension for each gender or race, or intersection thereof.
Each dimension vector holds fuzzy (in the range [0,1]) or
binary values indicating the presence of the respective
attribute in data samples.
The most common use case is to
define a multidimensional sensitive attribute
for intersectional fairness, like below. There,
the fb.categories@ operator unpacks genders and
races from a discrete iterable into dictionaries
of binary-valued vectors and then these are combined
into one multidimensional object.
The intersectional transformation
lets the original dimensions White, Black, etc.
account for attribute value intersections too,
like Male&White, Male&Black, etc.
import fairbench as fb
gender = ["Male", "Male", "Female", "Female", "Nonbinary"] # any iterable
race = ["Black", "White", "White", "Black", "White"] # any iterable
sensitive = fb.Dimensions(fb.categories@ gender, fb.categories@ race)
sensitive = sensitive.intersectional()
print(sensitive)
White [0 1 1 0 1] Black [1 0 0 1 0] Male [1 1 0 0 0] Male&White [0 1 0 0 0] Male&Black [1 0 0 0 0] Nonbinary [0 0 0 0 1] Nonbinary&White [0 0 0 0 1] Female [0 0 1 1 0] Female&White [0 0 1 0 0] Female&Black [0 0 0 1 0]
Explicit dimensions
Keywords like men, women, and nonbinary below, followed by non-dictionary values, declare dimension names. Dimension values will usually be lists, numpy arrays or deep learning tensors. Provide any number of dimensions.
import fairbench as fb
sensitive = fb.Dimensions(
men=[1, 1, 0, 0, 0],
women=[0, 0, 1, 1, 0],
nonbinary=[0, 0, 0, 0, 1]
)
Access dimension vectors
as either members of the Dimensions object
or as dictionary entries. Those vectors are
stored as eagerpy
tensors. Use eagerpy methods on Dimensions
to obtain a transformation with different values for each
dimension. Mainly, this is useful for summing attribute
vector values to get a sense of corresponding demographic
group sizes.
print(sensitive.nonbinary.numpy())
print(sensitive["nonbinary"].numpy()) # does the same
print(sensitive.sum())
NumPyTensor(array([0, 0, 0, 0, 1])) NumPyTensor(array([0, 0, 0, 0, 1])) men 2 women 2 nonbinary 1
From dictionaries
To set dimensions names programmatically or use names with special characters, such as spaces, pass as a positional argument to the constructor a dictionary that maps names to values. Do this with any number of dictionaries, and in addition to branches declared via keyword arguments.
import fairbench as fb
sensitive = fb.Dimensions(
{"non-binary": [0, 0, 0, 0, 1]},
men=[1, 1, 0, 0, 0],
women=[0, 0, 1, 1, 0]
)
print(sensitive.women.numpy()) # .numpy() is an eagerpy method
print(sensitive["non-binary"].numpy())
[0, 0, 1, 1, 0] [0, 0, 0, 0, 1]
Freely mix dimensions of multiple sensitive attributes.
That is, treat each value of each dimension as a separate dimension.
For example, mix in gender and age sensitive attributes
in the same Dimensions object like so.
import fairbench as fb
sensitive = fb.Dimensions(
men=[1, 1, 0, 0, 0],
nonmen=[0, 0, 1, 1, 1],
IsOld=[0, 1, 0, 1, 0]
)
To add multiple sensitive attributes without worrying about conflicting names, pass dictionaries as keyword arguments. This prepends the keyword argument name to all generated branch names.
import fairbench as fb
sensitive = fb.Dimensions(
gender={"1": [0, 0, 1, 1, 0], "0": [1, 1, 0, 0, 0], "?": [0, 0, 0, 0, 1]},
isold={"1": [0, 1, 0, 1, 0], "0": [1, 0, 1, 0, 1]}
)
gender1 [0 0 1 1 0] gender0 [1 1 0 0 0] gender? [0 0 0 0 1] isold1 [0 1 0 1 0] isold0 [1 0 1 0 1]
Unpack to dictionaries
FairBench offers helper operators that convert iterable
data into dictionaries that can be passed to Dimensions.
The most common pattern is analyzing categorical
values found in iterables with the
fb.categories@ operator.
For example, when applied on a list whose entries are among
"Man". "Woman", "Nonbin"
this creates three dimensions storing binary membership for each
of those genders.
import fairbench as fb
sensitive = fb.Dimensions(fb.categories@["Man", "Woman", "Man", "Woman", "Nonbin"])
print(sensitive)
Woman [0 1 0 1 0] Man [1 0 1 0 0] Nonbin [0 0 0 0 1]
Add the outcomes of multiple category analyses
to Dimensions with the patterns
already seen.
Any Python iterable can be analyzed into categories.
This includes lists, pandas dataframe
columns, categorical tensors, and numpy arrays.
import fairbench as fb
gender = ... # iterable (e.g., list) of gender attribute for each data sample
race = ... # iterable (e.g., list) of race attribute for each data sample
sensitive = fb.Dimensions(
gender=fb.categories@ gender,
race=fb.categories@ race
)
Use the fuzzy@ operator to unpack numeric iterables into two
fuzzy sensitive attributes; the first of those contains a
normalization to the range [0,1] and the second its complement.
import fairbench as fb
age = [18, 20, 19, 42, 30, 60, 18, 50, 40] # in years
sensitive = fb.Dimensions(age=fb.fuzzy@ age)
print(sensitive)
agelarge 60.000 [0. 0.04761905 0.02380952 0.57142857 0.28571429 1. 0. 0.76190476 0.52380952] agesmall 18.000 [1. 0.95238095 0.97619048 0.42857143 0.71428571 0. 1. 0.23809524 0.47619048]
Info
All FairBench measures accept fuzzy-based weighting of group membership by treating fuzzy numbers as the probability of exhibiting value of 1 and outputting the average expected value of computations.
Intersectionality
When dealing with multiple sensitive attributes,
dimensions for different attributes will often have
overlapping non-zero values. This means that
certain groups may intersect. For example,
some blacks may also be women.
To consider intersectional definitions of fairness,
create all dimension combinations with at least one
data sample by calling the intersectional method
of dimensions.
import fairbench as fb
sensitive = fb.Dimensions(
gender=fb.categories@["Man", "Woman", "Man", "Nonbin"],
race=fb.categories@["Other", "Black", "White", "White"]
)
sensitive = sensitive.intersectional()
The method for creating intersectional groups accepts parameter min_size that is the minimum number of data samples in each intersection. By default, that has value of 1 to admit intersections of at least one element, but you can set higher thresholds to prevent too small subgroups from creating non-robust analys. You can also set it that value zero to obtain all group intersections. If you are working with continuously-valued sensitive attributes, this is the sum of scores across considered groups.
Note that intersectional analysis considers both the intersections
and the original groups, because there is a chance that biases
could be uncovered only when looking at broader group levels.
If you want to keep only a set of groups by removing all those
that have been split into, further apply .strict(). An example
of these concepts follows:
import fairbench as fb
x, y, yhat = fb.bench.tabular.bank()
sensitive = fb.Dimensions(marital=fb.categories @ x["marital"], education=fb.categories @ x["education"])
sensitive = sensitive.intersectional(min_size=100).strict()
print(sensitive) # only a few large subgroups are retained
maritalsingle&educationtertiary [0 0 1 ... 0 0 0] maritalsingle&educationprimary [0 0 0 ... 0 0 0] maritalsingle&educationsecondary [0 0 0 ... 0 0 0] maritalsingle&educationunknown [0 0 0 ... 0 0 0] maritalmarried&educationtertiary [0 0 0 ... 0 0 0] maritalmarried&educationprimary [0 0 0 ... 1 0 1] maritalmarried&educationsecondary [0 1 0 ... 0 1 0] maritalmarried&educationunknown [0 0 0 ... 0 0 0] maritaldivorced&educationtertiary [0 0 0 ... 0 0 0] maritaldivorced&educationprimary [0 0 0 ... 0 0 0] maritaldivorced&educationsecondary [1 0 0 ... 0 0 0]
Danger
Intersectional analysis already considers all subgroup combinations and not only pairwise ones. It therefore runs in time O(m! n) where n is the number of samples in the population and m the number of groups. Applying it twice in succession has no value (no new groups would be found), and is computationally intractable if many interesections exist.