As society transitions to an economy driven by artificial intelligence (AI), an increasing number of prediction tasks is delegated to AI tools. Sometimes these tasks are within socially sensitive domains, such as determining credit-score ratings or predicting recidivism during parole. In the process, it has been recognized that machine learning algorithms are capable of learning societal biases, which we might not want them to learn, for example with respect to race or gender . This realization seeded an important debate in the machine learning community about fairness of algorithms and their impact on decision-making.
The first step towards understanding algorithmic fairness is about providing a formal definition of what fairness (or discrimination) means. In light of this, existing intuitive notions have been mathematically formalized, thereby also providing fairness metrics that can be used to quantify discrimination. However, various different notions of fairness exist, and these are sometimes mutually incompatible , meaning they cannot be satisfied at the same time for a given predictor Ŷ. In fact, there is currently no consensus on which notion of fairness is the correct one. Among the many proposal discussed in the literature, the most commonly considered ones include:
Apart from choosing a notion of fairness most appropriate to the setting that is analyzed, it is instructive to distinguish between two somewhat different tasks in fairness analysis. The first, usually simpler task is that of bias quantification, or bias measurement. In this case, we are interested in computing metrics from our dataset, in order to verify whether a definition is satisfied or not. Concretely, suppose we have a dataset in which Ŷ is the predicted salary of an employee and A is sex. If we are interested in demographic parity, Ŷ⊥ ⊥A, we can compute the average difference in salary between the male/female groups, that is $$\mathbbm{E}[\widehat{Y} \mid A = \text{male}] - \mathbbm{E}[\widehat{Y} \mid A = \text{female}].$$ Based on this quantity (which is known as total variation, or TV for short), we might decide that either there is a disparity between sexes, or perhaps not. For other definitions of fairness, different metrics would be appropriate.
Sometimes performing the first step of bias measurement is not the end goal. Suppose that we found a large salary gap between sexes in the example above, raising issues about possible discrimination. We then might be interested in correcting this bias, by computing new, more fair predictions (in which, say, the salary gap is lower). This second task is that of bias removal, or bias mitigation, in which we want to remove the undesired bias from our predictions.
Different software tools can be used to perform the two fairness tasks described above. In Figure we provide a graphical overview of some of the available software in and , which are the languages most commonly used for fairness analysis. For a given task and outcome/definition of fairness, we show the existing software packages that the reader might want to use. Since calibration is often satisfied by fitting an unconstrained model, our focus is on demographic parity and equality of odds.
The software landscape for fair ML in is more mature. Currently three well developed packages exist, capable of computing various fairness metrics. Also, these tools support training predictors that satisfy either equality of odds or demographic parity. These repositories are maintained by IBM, maintained by Microsoft and . A further package, , is narrower in scope but suitable for computing fairness metrics on very large datasets.
For , i.e., distributed via CRAN, there are fewer packages that relate to fair machine learning (see Figure ). Available packages include , which implements the non-convex method of , as well as and , which serve as diagnostic tools for measuring algorithmic bias and provide several pre- and post-processing methods for bias mitigation. The package described in this manuscript is a bias removal method which is able to interpolate between demographic parity and calibration notions, and is applicable to both regression and classification settings. In particular, is the only software in Figure which is causally aware. This means that the bias removal performed by can be explained by and related to the causal mechanisms that generated the discrimination in the first place.
The discussion on algorithmic fairness is, however, not restricted to the machine learning domain. There are many legal and philosophical aspects that are paramount. For example, the legal distinction between the disparate impact and disparate treatment doctrines is important for assessing fairness from a legal standpoint. This in turn emphasizes the importance of the interpretation behind the decision-making process, which is often not the case with black-box machine learning algorithms. For this reason, research in fairness through a causal inference lens gained attention.
A possible approach to fairness is the use of counterfactual reasoning , which allows for arguing what might have happened under different circumstances that never actually materialized, thereby providing a tool for understanding and quantifying discrimination. For example, one might ask how a change in sex would affect the probability of a specific candidate being accepted for a given job opening. This approach has motivated another notion of fairness, termed counterfactual fairness , which states that the decision made, should remain fixed, even if, hypothetically, the protected attribute such as race or gender were to be changed (this can be written succinctly as Ŷi(a) = Ŷi(a′) in the potential outcomes notation). Causal inference can also be used for decomposing the total variation measure into its direct, mediated, and confounded contributions , yielding further insights into demographic parity as a criterion. Furthermore, by introducing the notion of so-called resolving variables, described relaxations of demographic parity, which can possibly be a prohibitively strong notion.
The following sections describe an implementation of the fair data adaptation method outlined in , which combines the notions of counterfactual fairness and resolving variables, and explicitly computes counterfactual instances for individuals. The implementation is available as the -package from CRAN.
A first version of was published with the original manuscript . The software has since been developed further and novelty in the package presented in this manuscript includes the following:
fairadapt
and
fairadaptBoot
, alongside associated methods, provides a
more formalized implementation.The rest of the manuscript is organized as follows. In Section we describe the methodology behind , together with reviewing some important concepts of causal inference. In Section we discuss implementation details and provide some general user guidance, followed by Section in which we discuss how to perform uncertainty quantification with . Section illustrates the use of through a large, real-world dataset and a hypothetical fairness application. Finally, in Section we elaborate on some extensions, such as Semi-Markovian models and resolving variables.
In this section, the intuition behind is described using an example. This is followed by a more rigorous mathematical formulation, based on Markovian structural causal models (SCMs). Some relevant extensions, such as the Semi-Markovian case and the introduction of resolving variables are discussed in Section .
Consider for example a dataset about students applying for university admission. Let variable A, gender, be the protected attribute (A = a corresponding to females and A = a′ to males). Let E denote educational achievement (measured for example by grades achieved in school) and T the result of an admissions test for further education. Finally, let Y be the outcome of interest (final score) upon which admission to further education is decided. A visual representation of how the variables affect each other is given in Figure .
Attribute A, gender, has a causal effect on variables E, T, and Y. We consider this effect discriminatory and wish to eliminate it, in the following way. For each individual with observed values (a, e, t, y) we want to find a mapping
(a, e, t, y) → (a(fp), e(fp), t(fp), y(fp)),
which represents the value the person would have obtained in an alternative world where everyone was female. To construct such a mapping, we adapt (transform) the variables in the dataset in order. Explicitly, to a male person with education value e, we assign the transformed value e(fp), chosen such that
$$\mathbbm{P}(E \geq e \mid A = a') = \mathbbm{P}(E \geq {e}^{(fp)} \mid A = a).$$
The key idea is that the relative educational achievement within the subgroup remains preserved if the protected attribute gender is changed. If, for example, a male person has a higher educational achievement value than 70% of males in the dataset, we assume that he would also be better than 70% of females, had he been female1. After computing transformed educational achievement values corresponding to the female world (E(fp)), the transformed test score values T(fp) can be calculated in a similar fashion, but conditional on educational achievement. That is, a male with values (E, T) = (e, t) is assigned a test score t(fp) such that
$$\mathbbm{P}(T \geq t \mid E = e, A = a') = \mathbbm{P}(T \geq {t}^{(fp)} \mid E = {e}^{(fp)}, A = a),$$
where the value e(fp) was obtained in the previous step. This step can be visualized as shown in Figure .
In the final step, the outcome variable Y needs to be adjusted. The adaptation is based on the same principle as above, using transformed values of both education and the test score. The transformed value y(fp) of Y = y is chosen to satisfy
$$\mathbbm{P}(Y \geq y \mid E = e, T = t, A = a') = \mathbbm{P}(Y \geq {y}^{(fp)} \mid E = {e}^{(fp)}, T = {t}^{(fp)}, A = a).$$
The form of counterfactual correction described above is known as recursive substitution . We formalize this approach in the following sections. The reader who is satisfied with the intuitive notion provided by the above example is encouraged to go straight to Section .
In order to describe the causal mechanisms of a system, a structural causal model (SCM) can be hypothesized, which fully encodes the assumed data-generating process. An SCM is represented by a 4-tuple $\langle V, U, \mathcal{F}, \mathbbm{P}(u) \rangle$, where
Any particular SCM is accompanied by a graphical model 𝒢 (a directed acyclic graph, DAG). The set of variables that are inputs of the mechanism fVi are the parents of Vi denoted by pa(Vi). Therefore, the graph encodes how variables affect one another. Furthermore, we also write ch(Vi), de(Vi), an(Vi) for the children, descendants, and ancestors of Vi in the graph 𝒢. We assume throughout, without loss of generality, that
Equipped with the notion of an SCM and the assumptions (i)-(ii), we can describe the adaptation procedure in the Markovian case, in which all exogenous variables Ui are mutually independent (for the Semi-Markovian case, where variables Ui are allowed to share information, see Section ).
Let Y be the outcome of interest, taking values in $\mathbbm{R}$. Let A be the binary protected attribute taking two values a, a′. Denote by X the remaining covariates, and let V = (A, X, Y) denote the observed variables. Our goal is to describe a pre-processing method which transforms the observed variables V into their fair version V(fp). This is achieved by computing the counterfactual values V(A = a), which would have been observed if the protected attribute was fixed to a baseline value A = a for the entire sample.
More formally, going back to the university admission example above, we want to align the distributions
Vi ∣ pa(Vi), A = a and Vi ∣ pa(Vi), A = a′, meaning that the distribution of Vi conditional on pa(Vi) should be indistinguishable between female and male groups (and this should hold for every variable Vi). Since each function fi of the original SCM is reparametrized so that fi(pa(vi), ui) is increasing in ui for every fixed pa(vi), and since variables Ui are uniformly distributed on [0, 1], the Ui values can be interpreted as the latent quantiles associated with Vi. These latent quantiles are assumed to be preserved when performing the adaptation procedure.
The fair data adaption algorithms starts by fixing A = a for all individuals. After this, the algorithm iterates over descendants of the protected attribute A, in any valid topological order (this topological order is inferred from the causal graph 𝒢, which is also an input of the algorithm). For each Vi, the assignment function fi and the corresponding quantiles Ui are inferred. Finally, transformed values Vi(fp) are obtained by evaluating fi, using quantiles Ui and the transformed parents pa(Vi)(fp) (see Algorithm ).
The assignment functions fi of the SCM are always assumed to be unknown, but are inferred non-parametrically at each step. Algorithm obtains the counterfactual values V(A = a) under the do(A = a) intervention for each individual, while keeping the latent quantiles U fixed. In the case of continuous variables, the latent quantiles U can be determined exactly, while for the discrete case, the situation is more subtle. A detailed discussion can be found in .
The main function for data adaption in the package is
fairadapt()
. This function returns an S3 classed object of
class fairadapt
. The fairadapt
class has
associated implementations of the base S3 generics print()
,
summary()
, plot()
and predict()
.
Furthermore, methods are available for the autoplot()
generic exported from , as well as fairadapt
-specific
implementations of S3 generics visualizeGraph()
,
adaptedData()
, quantFit()
, and
fairTwins()
.
The following sections describe the intended use of
fairadapt()
, together with the associated methods and their
relations. The most important arguments of fairadapt()
include:
formula
: Argument of type formula
,
specifying the dependent and explanatory variables.adj.mat
: Argument of type matrix
, encoding
the adjacency matrix.train.data
and test.data
: Both of type
data.frame
, representing the respective datasets.prot.attr
: Scalar-valued argument of type
character
identifying the protected attribute. Has to
correspond to a column name in the train.data
argument.It is worth clarifying the possible data types that can be used in
the train.data
argument. We note the following:
As an example, we perform fair data adaption on the university
admission dataset described in Section . We load the
uni_admission
dataset provided by (inspired by the Berkeley
admissions dataset ), consisting of synthetic university admission data
of 1000 students. We subset the data, using the first
n_samp
rows as training data (uni_trn
) and the
following n_samp
rows as testing data
(uni_tst
). Furthermore, we construct an adjacency matrix
uni_adj
with edges gender
→ edu
, gender
→ test
,
edu
→ test
,
edu
→ score
,
and test
→ score
,
corresponding to the causal graph from Figure . We set
gender
as the protected attribute.
n_samp <- 500
uni_dat <- data("uni_admission", package = "fairadapt")
uni_dat <- uni_admission[seq_len(2 * n_samp), ]
head(uni_dat)
## gender edu test score
## 1 1 1.3499572 1.617739679 1.9501728
## 2 0 -1.9779234 -3.121796235 -2.3502495
## 3 1 0.6263626 0.530034686 0.6285619
## 4 1 0.8142112 0.004573003 0.7064857
## 5 1 1.8415242 1.193677123 0.3678313
## 6 1 -0.3252752 -2.004123561 -1.5993848
uni_trn <- head(uni_dat, n = n_samp)
uni_tst <- tail(uni_dat, n = n_samp)
uni_dim <- c( "gender", "edu", "test", "score")
uni_adj <- matrix(c( 0, 1, 1, 0,
0, 0, 1, 1,
0, 0, 0, 1,
0, 0, 0, 0),
ncol = length(uni_dim),
dimnames = rep(list(uni_dim), 2),
byrow = TRUE)
set.seed(2022)
basic <- fairadapt(score ~ ., train.data = uni_trn,
test.data = uni_tst, adj.mat = uni_adj,
prot.attr = "gender")
basic
##
## Call:
## fairadapt(formula = score ~ ., prot.attr = "gender", adj.mat = uni_adj,
## train.data = uni_trn, test.data = uni_tst)
##
##
## Adapting variables:
## score, edu, test
##
## Based on protected attribute gender
##
## AND
##
## Based on causal graph:
## score gender edu test
## score 0 0 0 0
## gender 0 0 1 1
## edu 1 0 0 1
## test 1 0 0 0
The implicitly called print()
method in the previous
code block displays some information about how fairadapt()
was called. This information includes the variables that were adapted,
the protected attribute, and the causal graph used for the adaptation
(printed as an adjacency matrix). By additionally calling the
summary()
function, we can inspect the number of training
an test samples, and the total variation before and after adaptation,
written in our notation as
$$\mathbbm{E}[Y \mid A = a] - \mathbbm{E}[Y \mid A = a'] \text{ and } \mathbbm{E}[ {Y}^{(fp)} \mid A = a] - \mathbbm{E}[ {Y}^{(fp)} \mid A = a'],$$ respectively, shown below:
##
## Call:
## fairadapt(formula = score ~ ., prot.attr = "gender", adj.mat = uni_adj,
## train.data = uni_trn, test.data = uni_tst)
##
## Protected attribute: gender
## Protected attribute levels: 0, 1
## Adapted variables: edu, test, score
##
## Number of training samples: 500
## Number of test samples: 500
## Quantile method: rangerQuants
##
## Total variation (before adaptation): -0.7045
## Total variation (after adaptation): -0.07534
The adapted train and test data can be obtained using the
adaptedData()
function and passing the argument
train = TRUE
for the training data, and
train = FALSE
for the test data:
## gender edu test
## 501 0 -2.2844949 -1.3101484
## 502 0 -0.3993245 -1.2148590
## 503 0 0.4161544 0.6885127
## 504 0 -0.8676345 -1.4806227
## 505 0 -0.3513931 -0.5215339
## 506 0 -0.2337741 -1.0333720
The algorithm used for fair data adaption in fairadapt()
is based on graphical causal model 𝒢
(see Algorithm ). To specify the causal graph, we pass the corresponding
adjacency matrix as the adj.mat
argument. The convenience
function graphModel()
turns a graph specified as an
adjacency matrix into an annotated graph using the package .
Alternatively, by calling the S3 generic visualizeGraph()
on a fairadapt
object, the user can also inspect the
graphical model that was used for the data adaptation.
A visualization of the igraph
object returned by
graphModel()
is shown in Figure . The graph is the same as
that in Figure . However, specifying the causal graph is not the only
option to perform the data adaptation. A possible alternative is to
specify a valid topological ordering over the observable variables V, and specify it as a
character
vector, using the top.ord
argument.
The most common and recommended usage of fairadapt()
follows a typical machine learning framework. We start by calling the
fairadapt()
function that performs the quantile learning
step and counterfactual correction, based on the train.data
argument. Following this, we can call the predict()
function on the returned fairadapt
S3 object, in order to
perform data adaption on new test data. In such a workflow, the
adaptation of the training and testing data is done separately. In
specific situations, it might be desirable to input both
train.data
and test.data
arguments directly to
fairadapt()
, which then transforms both the training and
testing data jointly. This one-step procedure might be considered when
the proportion of test samples compared to train samples is large, and
when the train.data
has a relatively small sample size. The
benefit of this approach is that, even though the outcome Y is not available, other attributes
X of test.data
can be used for quantile learning step.
The data frames passed as train.data
and
test.data
are required to have column names which also
appear in the row and column names of the adjacency matrix. The
protected attribute A, passed
as scalar-valued character vector prot.attr
, should also
appear in the column names of train.data
and
test.data
. The test.data
argument defaults to
NULL
, with the intention that test.data
is
specified as an input to the predict()
function at a later
stage.
The quantile learning step of Algorithm can in principle be carried out by several methods, three of which are implemented in :
Using linear quantile regression is the most efficient option in
terms of runtime, while for non-parametric models and mixed data, the
random forest approach is well-suited, at the expense of a slight
increase in runtime. The neural network approach is substantially slower
when compared to linear and random forest estimators and consequently
does not scale well to large sample sizes. As default, the random forest
based approach is used, due to its non-parametric nature and
computational speed. However, for smaller sample sizes, the neural
network approach can also demonstrate competitive performance. In Table
we provide a quick summary outlining some differences between the three
natively supported methods, and also report runtimes on the
uni_admission
dataset with different sample sizes.
The quantile methods shown in Table make calls to specific functions
that perform quantile regression. These functions take varying arguments
and for that reason, fairadapt()
forwards arguments passed
as ...
to the function specified as
quant.method
.
An important consideration in choosing values for optional arguments
of specific quantile regression functions is computational speed. For
example, the function rangerQuants()
internally calls the
ranger()
function (from ) and with respect to computational
speed, an important argument is num.trees
, the number of
trees used when building the quantile regression forest. Clearly,
choosing a smaller number of trees will be faster, but at the same time
will result in a fit with larger variance.
Similarly, mcqrnnQuants()
internally calls
mcqrnn.fit()
(from ), which has a number of arguments that
can be used for adapting the underlying neural network. In terms of
computational speed, the most important arguments are
n.trials
(number of repeated initializations used to avoid
local minima) and iter.max
(maximum number of iterations of
the optimization). Choosing smaller values will reduce the runtime.
Lastly, function linearQuants()
internally calls
rq()
(from ). This function is less flexible, since the
model is linear. However, its method
argument can be used
when the number of samples becomes large (using method
equal to "fn"
or "pfn"
utilizes the
Frisch-Newton interior point method, which may be preferable for large
samples).
Both rangerQuants()
and mcqrnnQuants()
expose flexible machine learning tools with several parameters that
impact fit quality. In order to optimize the fitting procedure by tuning
these parameters, we need a way of assessing the quality of our fit. In
the context of quantile regression we can estimate the expected τ-quantile loss function,
where μτ(pa(Vi)) is the function predicting the τ-quantile of variable Vi using the parents pa(Vi) and ρτ is the asymmetric L1 loss function whose minimizer is the τ-quantile. The function ρτ is given by
$$ \rho_{\tau}(x, y) = \begin{cases} \phantom{( )}\tau(x-y), & \text{ for } x \geq y\\ (1-\tau)(y-x), & \text{ for } x < y. \end{cases} $$
A smaller empirical loss based on Equation corresponds to better fit
quality. For hyperparameter tuning we can perform cross-validation
(fitting the quantile regression on separate folds), which is directly
available within the fairadapt()
function. The argument
eval.qfit
has a default value NULL
, but if
this argument is given a positive integer value, then it is used as the
number of folds for performing cross-validation.
We compute the average empirical loss $\widehat{\mathbbm{E}} [\rho_{\tau} (V_i,
\mu_{\tau}(\mathrm{pa}(V_i)))]$ for each variable Vi and τ = 0.25, 0.5, 0.75 (corresponding
to 25%, 50% and 75% quantiles). The average of these three values is
reported at the end, and can be extracted from the resulting
fairadapt
object using the quantFit()
method:
set.seed(22)
fit_qual <- fairadapt(score ~ ., train.data = uni_trn,
adj.mat = uni_adj, prot.attr = "gender",
eval.qfit = 3L)
quantFit(fit_qual)
## edu test score
## 0.3446551 0.2796783 0.3428743
The function returns the quality of the quantile fit for each variable. A very reasonable objective to minimize is the average of these values, by iterating over a grid of possible values of the tuning parameters. The interesting parameters to optimize for the two methods include:
ranger()
: parameters mtry
(number of
candidate variables considered for splitting in each step),
min.node.size
(size of leaf nodes below which splitting is
stopped) and max.depth
(maximum depth of each tree),mcqrnn()
: parameters n.hidden
,
n.hidden2
(number of nodes in the first and second hidden
layers), Th
(activation function), method
(optimizer to be used), and a range of other parameters that are fed to
the chosen optimizer via ellipsis.The quantile methods included in have reasonable default values, that serve as a good starting point. Optimizing the quantile fit should therefore be of interest mostly to advanced users (and hence we do not perform parameter tuning here explicitly).
The above-mentioned set of methods is not exhaustive. Further options
are conceivable and therefore provides an extension mechanism to allow
for custom quantile method specified by the user. The
fairadapt()
argument quant.method
expects a
function to be passed, a call to which will be constructed with three
unnamed arguments:
data.frame
containing data to be used for quantile
regression. This will either be the data.frame
passed as
train.data
, or if test.data
was specified, a
concatenated, row-bound version of train.data
and
test.data
.logical
vector of length nrow(data)
,
indicating which rows in the data.frame
passed as
data
correspond to samples with baseline values of the
protected attribute.Arguments passed as ...
to fairadapt()
will
be forwarded to the function specified as quant.method
and
passed after the first three fixed arguments listed above. The return
value of the function passed as quant.method
is expected to
be an S3-classed object. This object should represent the conditional
distribution Vi ∣ pa(Vi)
(see function rangerQuants()
for an example). Additionally,
the object should have an implementation of the S3 generic function
computeQuants()
available. For each row (vi, pa(vi))
of the data
argument, the computeQuants()
function uses the S3 object to perform the following steps:
newdata
argument).For an example, see the computeQuants.ranger()
method
for a ranger
object, which can be invoked by the
computeQuants()
generic.
We now turn to a useful property of , which allows the user to
explore counterfactual instances for different individuals in the
dataset. The university admission example presented in Section
demonstrates how to compute counterfactual values for an individual
while preserving their relative educational achievement. Setting
candidate gender as the protected attribute and gender level
female as baseline value, for a male student with
values (a, e, t, y),
his fair-twin values (a(fp), e(fp), t(fp), y(fp)),
i.e., the values the student would have obtained, had he been
female, are computed. These values can be retrieved from a
fairadapt
object by calling the S3-generic function
fairTwins()
as:
## gender score score_adapted edu edu_adapted test
## 1 1 1.9501728 0.427458 1.3499572 0.8167695 1.6177397
## 2 0 -2.3502495 -2.350250 -1.9779234 -1.9779234 -3.1217962
## 3 1 0.6285619 1.098413 0.6263626 0.1834470 0.5300347
## test_adapted
## 1 0.8557834
## 2 -3.1217962
## 3 0.1141050
In this example, we compute the values in a female world. Therefore, for female applicants, the values remain fixed, while for male applicants the values are adapted, as can be seen from the output. Having access to explicit counterfactual instances as above may help justify fair decisions in practice or help guide the choice of the assumed causal model and resolving variables (see Section for resolving variables).
The user might naturally be interested in uncertainty quantification
of the procedure performed in fairadapt()
. In order to
explain how this can be achieved, we give a visualization of the typical
workflow when using fairadapt()
(see Figure ).
Such a workflow can be described as follows. We start from the
training data 𝒟train
and the causal graph 𝒢. These two
arguments are used as inputs of the fairadapt()
function,
which returns a fairadapt
S3 object (which also contains
the transformed test data $\widetilde{\mathcal{D}}_{train}$). The
fairadapt
object, together with the test data 𝒟test,
is then used as a input to the predict()
function. The
predict()
function returns the transformed test data $\widetilde{\mathcal{D}}_{test}$. Often, the
end goal is to obtain fair predictions on some new test data 𝒟test.
To do so, we need to train a classifier/regressor. Either the training
data 𝒟train
or its transformed counterpart $\widetilde{\mathcal{D}}_{train}$ can be used
for building a predictor. The predictor then needs to be applied to the
transformed train data $\widetilde{\mathcal{D}}_{test}$. Building on
the graphical visualization in Figure , which serves as a mental map of
our workflow, we can now explain the distinct sources of uncertainty
that can be considered:
Finite sample uncertainty: The first, commonly
encountered source of uncertainty is the one induced by finite sample
size. The training data 𝒟train
has a finite size, and for this reason inferences made using this data
are imperfect. We wish to quantify the uncertainty in the predictions
Ŷtestfair
introduced by the finite sample size of 𝒟train.
As Figure shows, the training data 𝒟train
affects the resulting fair predictions in two ways. Firstly, it affects
the value of the transformed test data $\widetilde{\mathcal{D}}_{test}$ (mediated by
the fairadapt
S3 object). Secondly, it affects the
predictions Ŷtestfair
through the predictor (since it is the input to the
regressor/classifier). These finite sample uncertainties can be analyzed
using bootstrap . This means that we repeat the procedure in
Figure many times, each time taking a different bootstrap sample of the
training data. Below we will show how this can be done with the
fairadaptBoot()
function.
Inherent uncertainty in the quantiles: A second
source of uncertainty arises from the uncertainty in quantile estimation
and is specific to . As described in Section (see also Figure ), the
fairadapt()
procedure aims to preserve the relative
quantile of the variable, when computing the do(A = a)
intervention. However, when we are working with variables that are not
continuous, defining a quantile becomes more difficult. Therefore, in
presence of discrete variables, due to the imperfect estimation of
quantiles, the fairadapt()
procedure has some inherent
randomness. This randomness would still persist even if we had infinite
training samples in 𝒟train.
Importantly, to achieve fair predictions, taking an expectation over
this randomness is not feasible. For a detailed discussion of why this
is the case, refer to .
For quantifying uncertainty, we use the fairadaptBoot()
function, where the most important arguments are:
formula
, prot.attr
, adj.mat
,
and train.data
arguments are the same as for the
fairadapt()
function (see Section ).test.data
, a data.frame
containing the
test data, defaults to NULL
. Whenever the test data equals
NULL
, then keep.object
must be
TRUE
.keep.object
, a logical
scalar, indicating
whether all the fairadapt
S3 objects built in bootstrap
repetitions should be kept in working memory. Default value is
FALSE
.rand.mode
, a character
scalar, taking
values "finsamp"
, "quant"
, or
"both"
, corresponding to considering finite sample
uncertainty, quantile uncertainty, or both.The function fairadaptBoot()
returns an S3 object of
class fairadaptBoot
. Calling this function can be
computationally expensive, both in terms of runtime and memory. Keeping
the default value of FALSE
for the keep.object
argument reduces the memory consumption substantially, with the drawback
that test.data
has to be provided directly to
fairadaptBoot()
, and that the resulting
fairadaptBoot
object cannot be reused for making
predictions at a later stage. Passing TRUE
to
save.object
, on the other hand, might consume more memory,
but then the object can be reused for transforming new test data. This
can be done by using the predict()
function for the
fairadaptBoot
S3 object, to which a newdata
argument is available.
For illustration purposes, we now compute bootstrap repetitions for finite sample uncertainty and quantile uncertainty on the COMPAS dataset . We begin by loading the COMPAS dataset and constructing its causal graph:
cmp_dat <- data("compas", package = "fairadapt")
cmp_dat <- get(cmp_dat)
cmp_mat <- matrix(0, nrow = ncol(cmp_dat), ncol = ncol(cmp_dat),
dimnames = list(names(cmp_dat), names(cmp_dat)))
cmp_mat[c("race", "sex", "age"),
c("juv_fel_count", "juv_misd_count",
"juv_other_count", "priors_count",
"c_charge_degree", "two_year_recid")] <- 1
cmp_mat[c("juv_fel_count", "juv_misd_count", "juv_other_count"),
c("priors_count", "c_charge_degree", "two_year_recid")] <- 1
cmp_mat["priors_count", c("c_charge_degree", "two_year_recid")] <- 1
cmp_mat["c_charge_degree", "two_year_recid"] <- 1
head(cmp_dat)
## sex age race juv_fel_count juv_misd_count juv_other_count
## 1 Male 69 Non-White 0 0 0
## 2 Male 34 Non-White 0 0 0
## 3 Male 24 Non-White 0 0 1
## 4 Male 23 Non-White 0 1 0
## 5 Male 43 Non-White 0 0 0
## 6 Male 44 Non-White 0 0 0
## priors_count c_charge_degree two_year_recid
## 1 0 F 0
## 2 0 F 1
## 3 4 F 1
## 4 1 F 0
## 5 2 F 0
## 6 0 M 0
The COMPAS dataset contains information on 7214 individuals from Broward County, Florida, who were released on parole. The variables include race, sex, age, juvenile offense counts, priors count and the degree of criminal charge. The outcome of interest is recidivism within two years and the protected attribute is race (taking values Non-White and White). A possible causal graph for the COMPAS dataset is given in Figure .
After loading the dataset, we run the fairadaptBoot()
function twice, with two different values of the rand.mode
argument. First, we consider only the finite sample uncertainty.
cmp_trn <- tail(cmp_dat, n = 100L)
cmp_tst <- head(cmp_dat, n = 100L)
n_itr <- 3L
set.seed(2022)
fa_boot_fin <- fairadaptBoot(two_year_recid ~ ., "race", cmp_mat,
cmp_trn, cmp_tst, rand.mode = "finsamp",
n.boot = n_itr)
Then, we re-run the bootstrap procedure, but by considering only the
inherent quantile uncertainty, by setting the rand.mode
argument to "quant"
.
set.seed(2022)
fa_boot_quant <- fairadaptBoot(two_year_recid ~ ., "race", cmp_mat,
cmp_trn, cmp_tst, rand.mode = "quant",
n.boot = n_itr)
The returned objects are of class fairadaptBoot
. The
object stores different replicates of the adapted test data
(n.boot
copies, the number of bootstrap repetitions) and
some metadata. To obtain predictions, we train a random forest
classifier on different bootstrap samples of train.data
,
and apply it to the transformed data bootstrap replicates. In doing so,
we make use of the boot.ind
list, contained in the
fairadaptBoot
object, representing row indices of all
bootstrap repetitions.
fit_rf <- function(x) {
ranger(factor(two_year_recid) ~ ., cmp_trn[x, ], probability = TRUE)
}
extract_pred <- function(x) x$predictions[, 2L]
set.seed(2022)
cmp_rf <- lapply(fa_boot_fin$boot.ind, fit_rf)
pred_fin <- Map(predict, cmp_rf, adaptedData(fa_boot_fin, train = FALSE))
pred_fin <- do.call(cbind, lapply(pred_fin, extract_pred))
pred_quant <- Map(predict, cmp_rf, adaptedData(fa_boot_quant, train = FALSE))
pred_quant <- do.call(cbind, lapply(pred_quant, extract_pred))
In order to analyze the different sets of predictions Ŷtestfair, two slightly different perspectives can be taken, and we elaborate on both in the following sections.
The first way to analyze the uncertainty of the predictions is from
the viewpoint of the decision-maker. By decision-maker, in this context
we refer to the individual performing the analysis and obtaining a set
of fair predictions. For a decision-maker, it is important to understand
how sensitive the outcome of the classification is to uncertainties
induced by both finite sample size and the inherent uncertainty induced
by discrete variables. Let pA be the vector
of predicted probabilities on the test.data
, with length
nrow(test.data)
. Denote nrow(test.data)
with
ntest.
Let pB be
another vector of predicted probabilities (under a different bootstrap
repetition). We can consider the following four metrics of
uncertainty:
jac_frm <- function(x, modes = "single") {
jac <- function(a, b) {
intersection <- length(intersect(a, b))
union <- length(a) + length(b) - intersection
intersection / union
}
res <- lapply(
seq(quantile(x[, 1L], 0.05), quantile(x[, 1L], 0.95), 0.01),
function(tsh) {
ret <- replicate(100L, {
col <- sample(ncol(x), 2L)
jac(which(x[, col[1L]] > tsh), which(x[, col[2L]] > tsh))
})
data.frame(tsh = tsh, y = mean(ret), sd = sd(ret),
mode = modes)
}
)
do.call(rbind, res)
}
jac_df <- rbind(jac_frm(pred_fin, "Finite Sample"),
jac_frm(pred_quant, "Quantiles"))
ord_ind <- function(x, modes = "single") {
res <- replicate(5000L, {
row <- sample(nrow(x), 2)
ord <- mean(x[row[1], ] > x[row[2], ])
max(ord, 1 - ord)
})
data.frame(res = res, mode = modes)
}
ord_df <- rbind(ord_ind(pred_fin, "Finite Sample"),
ord_ind(pred_quant, "Quantiles"))
inv_frm <- function(x, modes = "single") {
gt <- function(x) x[1L] > x[2L]
res <- replicate(100L, {
col <- sample(ncol(x), 2L)
prm <- order(x[, col[2L]][order(x[, col[1L]])])
sum(combn(prm, 2L, gt)) / choose(length(prm), 2L)
})
data.frame(res = res, mode = modes)
}
inv_df <- rbind(inv_frm(pred_fin, "Finite Sample"),
inv_frm(pred_quant, "Quantiles"))
prb_frm <- function(x, modes = "single") {
qnt <- apply(x, 1L, quantile, probs = c(0.05, 0.95))
data.frame(width = qnt[2L, ] - qnt[1L, ], mode = modes)
}
prb_df <- rbind(prb_frm(pred_fin, "Finite Sample"),
prb_frm(pred_quant, "Quantiles"))
The results of these four metrics applied to different bootstrap repetitions on the COMPAS dataset are shown in Figure .
## Warning: A numeric `legend.position` argument in `theme()` was deprecated in
## ggplot2 3.5.0.
## ℹ Please use the `legend.position.inside` argument of `theme()`
## instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning
## was generated.
## Warning: Removed 2 rows containing missing values or values outside the scale
## range (`geom_point()`).
## Warning: Removed 2 rows containing missing values or values outside the scale
## range (`geom_line()`).
## Warning: Removed 137 rows containing non-finite outside the scale range
## (`stat_density()`).
The other point of view we can take in quantifying uncertainty is that of the individual experiencing fair decisions. In this case, we are not so much interested in how much the overall decision set changes (as was the case above), but rather how much variation there is in the estimate for the specific individual. To this end, one might investigate the spread of the predicted values for a specific individual. The spread of the predictions for the first three individuals can be obtained as follows:
ind_prb <- data.frame(
prob = as.vector(t(pred_quant[seq_len(3), ])),
individual = rep(c(1, 2, 3), each = fa_boot_quant$n.boot)
)
This spread is visualized in Figure . While it might be tempting to
take the mean of the different individual predictions, to have more
stable results, this should not be done, as such an approach does not
guarantee the fairness constraint fairadapt()
aims to
achieve. Hence, in order to achieve the desired fairness criteria
overall, we have to accept some level of randomization at the level of
individual predictions. It would be interesting for future work to
quantify and optimize explicitly the trade-off between the desired
fairness criteria and the necessary level of randomization.
As a hypothetical real-world use of , suppose that after a legislative change the US government has decided to adjust the salary of all of its female employees in order to remove both disparate treatment and disparate impact effects. To this end, the government wants to compute the counterfactual salary values of all male employees, that is the salaries that male employees would obtain, had they been female (i.e., the female group serves as the baseline).
To do this, the government is using data from the 2018 American
Community Survey by the US Census Bureau. This dataset is also available
in pre-processed form as a package dataset from . Columns are grouped
into demographic (dem
, including age, race, origin,
citizenship, and economic region), familial (fam
, including
marital status, size of the family, and number of children), educational
(edu
, including number of years spent in schooling and the
level of English proficiency) and occupational (occ
,
including the hours and week worked every year, occupation category, and
industry of employment) categories and finally, salary is selected as
response (res
) and sex as the protected attribute
(prt
):
gov_dat <- data("gov_census", package = "fairadapt")
gov_dat <- get(gov_dat)
dem <- c("age", "race", "hispanic_origin", "citizenship",
"nativity", "economic_region")
fam <- c("marital", "family_size", "children")
edu <- c("education_level", "english_level")
occ <- c("hours_worked", "weeks_worked", "occupation",
"industry")
prt <- "sex"
res <- "salary"
The hypothesized causal graph for the dataset is given in Figure .
According to this, the causal graph can be specified as an adjacency
matrix gov_adj
and as confounding matrix
gov_cfd
:
cols <- c(dem, fam, edu, occ, prt, res)
gov_adj <- matrix(0, nrow = length(cols), ncol = length(cols),
dimnames = rep(list(cols), 2))
gov_cfd <- gov_adj
gov_adj[dem, c(fam, edu, occ, res)] <- 1
gov_adj[fam, c( edu, occ, res)] <- 1
gov_adj[edu, c( occ, res)] <- 1
gov_adj[occ, res ] <- 1
gov_adj[prt, c(fam, edu, occ, res)] <- 1
gov_cfd[prt, dem] <- 1
gov_cfd[dem, prt] <- 1
gov_grph <- graphModel(gov_adj, gov_cfd)
A visualization of the full graph using is shown in Figure .
## Warning: No shared levels found between `names(values)` of the manual scale
## and the data's linetype values.
Before applying fairadapt()
, we first log-transform the
salaries to avoid dealing with a possibly heavy-tailed distribution for
which quantile estimation may be more difficult. We then inspect the
densities of variable salary
by sex group, as shown in
Figure A. There is a clear shift between the two distributions,
indicating that male
employees are better compensated than
female
employees. We perform the adaptation by using
n_samp
samples for training and n_pred
samples
for testing.
gov_dat$salary <- log(gov_dat$salary)
gov_trn <- head(gov_dat, n = n_samp)
gov_prd <- tail(gov_dat, n = n_pred)
set.seed(22)
gov_ada <- fairadapt(salary ~ ., train.data = gov_trn,
adj.mat = gov_adj, cfd.mat = gov_cfd,
prot.attr = prt)
After adapting the data, we investigate whether the salary gap has
become smaller. This can be done by comparing distributions of variable
salary
using the -exported S3 generic function
autoplot()
(Figure B).
## Warning in get_plot_component(plot, "guide-box"): Multiple components
## found; returning the first one. To return all, use `return_all =
## TRUE`.
For adapting additional testing data, we use the base S3 generic
function predict()
and output a selection of columns:
set.seed(2022)
gov_prd_ada <- predict(gov_ada, newdata = gov_prd)
gov_prd_ada[, c("sex", "age", "education_level", "salary")]
## sex age education_level salary
## 204305 female 19 16 7.003065
## 204306 female 46 18 9.667765
## 204307 female 24 19 10.126631
## 204308 female 23 19 9.903488
## 204309 female 50 19 11.472103
Finally, we can do fair-twin inspection using the
fairTwins()
function of , to retrieve counterfactual values
of some features for different individuals:
## sex age age_adapted salary salary_adapted
## 1 male 64 64 10.66896 10.51867
## 2 female 54 54 10.71442 10.71442
## 3 male 38 38 11.50288 11.41353
## 4 female 41 41 11.05089 11.05089
## 5 female 40 40 10.71885 10.71885
Note that values remain unchanged for female individuals (as
female was used as baseline level). Variable age
,
which is not a descendant of the protected attribute sex
(see Figure ), also remains unchanged. However, variables
education_level
and salary
do change for
males, since these variables are descendants of the protected attribute
sex
.
We conclude the section with a remark. Notice that the variable
hours_worked
is a descendant of A. However, one might argue that
this variable should not be adapted in the procedure, i.e., it
should remain the same, irrespective of employee sex. In other words,
one might argue it is acceptable to distinguish individuals based on
this variable. This is the idea behind resolving variables,
which are discussed in Section . It is worth emphasizing that we are not
answering the question of how to choose which variables are resolving -
this choice is left to social scientists familiar with the context of
the dataset.
Several extensions to the basic Markovian SCM formulation introduced in Section exist, and these are outlined in the following sections.
As we mentioned earlier, in some situations the protected attribute A might affect other variables in a non-discriminatory way. For instance, in the Berkeley admissions dataset we observe that females often apply for departments with lower admission rates and consequently have a lower admission probability. However, we perhaps would not wish to account for this difference in the adaptation procedure, if we were to argue that applying to a certain department is a choice everybody is free to make. Such examples motivated the idea of resolving variables by . A variable R is called resolving if
In presence of resolving variables, computation of the counterfactual
is carried out under the more involved intervention do(A = a, R = R(a′)).
The potential outcome value V(A = a, R = R(a′))
is obtained by setting A = a and computing the
counterfactual while keeping the values of resolving variables to those
they attained naturally. This is a nested counterfactual and
the difference in Algorithm is simply that resolving variables R are skipped in the for-loop. In
order to perform fair data adaptation with the variable
test
being resolving in the uni_admission
dataset used in Section , the string "test"
can be passed
as res.vars
to fairadapt()
.
res_basic <- fairadapt(score ~ ., train.data = uni_trn,
test.data = uni_tst, adj.mat = uni_adj,
prot.attr = "gender", res.vars = "test")
summary(res_basic)
##
## Call:
## fairadapt(formula = score ~ ., prot.attr = "gender", adj.mat = uni_adj,
## train.data = uni_trn, test.data = uni_tst, res.vars = "test")
##
## Protected attribute: gender
## Protected attribute levels: 0, 1
## Adapted variables: edu, score
## Resolving variables: test
##
## Number of training samples: 500
## Number of test samples: 500
## Quantile method: rangerQuants
##
## Total variation (before adaptation): -0.7045
## Total variation (after adaptation): -0.3027
As can be seen from the respective model summary outputs, the total variation after adaptation, in this case, is larger than in the example from Section , with no resolving variables. The intuitive reasoning here is that resolving variables allow for some discrimination, so we expect to see a larger total variation between the groups.
A visualization of the corresponding graph is available from Figure ,
which highlights the resolving variable test
in red, but
the underlying graphical model remains the same.
In Section we focused on the Markovian case, which assumes that all exogenous variables Ui are mutually independent. However, in practice, this requirement is often not satisfied. If a mutual dependency structure between variables Ui exists, we are speaking about Semi-Markovian models. In the university admission example, we might have that $U_{\text{test}} \not\!\perp\!\!\!\perp U_{\text{score}}$. That is, latent variables corresponding to variables test and final score being correlated. Such dependencies between latent variables can be represented by dashed, bidirected arrows in the causal diagram, as shown in Figures and .
There is an important difference in the adaptation procedure for the Semi-Markovian case: when inferring the latent quantiles Ui of variable Vi, in the Markovian case, only the direct parents pa(Vi) are needed. In the Semi-Markovian case, due to correlation of latent variables, using only the pa(Vi) can lead to biased estimates of the Ui. Instead, the set of direct parents needs to be extended, as described in more detail by . A brief sketch of the argument goes as follows: Let the C-components be a partition of the set V, such that each C-component contains a set of variables which are mutually connected by bidirected edges. Let C(Vi) denote the entire C-component of variable Vi. We then define the set of extended parents as
Pa(Vi) := (C(Vi) ∪ pa(C(Vi))) ∩ an(Vi),
where an(Vi) is the set of ancestors of Vi. The adaptation procedure in the Semi-Markovian case in principle remains the same as outlined in Algorithm , with the difference that the set of direct parents pa(Vi) is replaced by Pa(Vi) at each step.
To include the bidirected edges in the adaptation, we can pass a
matrix
as cfd.mat
argument to
fairadapt()
such that:
cfd.mat
has the same dimension, column and row names as
adj.mat
.cfd.mat
is symmetric.adj.mat
, an entry
cfd.mat[i, j] == 1
indicates that there is a bidirected
edge between variables i
and j
.The following code performs fair data adaptation of the
Semi-Markovian university admission variant with a mutual dependency
between the variables representing test and final scores. For this, we
create a matrix uni_cfd
with the same attributes as the
adjacency matrix uni_adj
and set the entries representing
the bidirected edge between vertices test
and
score
to 1. Finally, we
can pass this confounding matrix as cfd.mat
to
fairadapt()
. A visualization of the resulting causal graph
is available from Figure .
uni_cfd <- matrix(0, nrow = nrow(uni_adj), ncol = ncol(uni_adj),
dimnames = dimnames(uni_adj))
uni_cfd["test", "score"] <- 1
uni_cfd["score", "test"] <- 1
semi <- fairadapt(score ~ ., train.data = uni_trn,
test.data = uni_tst, adj.mat = uni_adj,
cfd.mat = uni_cfd, prot.attr = "gender")
Alternatively, instead of using the extended parent set Pa(Vi), we could
also use the entire set of ancestors an(Vi). This
approach is implemented as well, and available by specifying a
topological ordering. This is achieved by passing a
character
vector, containing the correct ordering of the
names appearing in names(train.data)
as
top.ord
argument to fairadapt()
. The benefit
of using this option is that the specific edges of the causal model
𝒢 need not be specified. However, in
the linear case, specifying the edges of the graph, so that the
quantiles are inferred using only the set of parents, will in principle
have better performance. The topological variant can be invoked as
follows:
set.seed(2022)
top_ord <- fairadapt(score ~ ., train.data = uni_trn, test.data = uni_tst,
top.ord = c("gender", "edu", "test", "score"),
prot.attr = "gender")
summary(top_ord)
##
## Call:
## fairadapt(formula = score ~ ., prot.attr = "gender", train.data = uni_trn,
## test.data = uni_tst, top.ord = c("gender", "edu", "test",
## "score"))
##
## Protected attribute: gender
## Protected attribute levels: 0, 1
## Adapted variables: edu, test, score
##
## Number of training samples: 500
## Number of test samples: 500
## Quantile method: rangerQuants
##
## Total variation (before adaptation): -0.7045
## Total variation (after adaptation): 0.02508
Note that the topological variant for the university admissions dataset is the same as supplying the adjacency matrix, since the causal graph has no missing edges.
So far we did not discuss whether it is always possible to carry out the counterfactual inference described in Section . In the causal literature, an intervention is termed identifiable if it can be computed uniquely using the data and the assumptions encoded in the graphical model 𝒢. An important result by states that an intervention do(X = x) on a variable X is identifiable if there is no bidirected path between X and ch(X). Therefore, our intervention of interest is identifiable if one of the two following conditions are met:
Based on this, the fairadapt()
function might return an
error, if the specified intervention is not possible to compute. An
additional limitation is that currently does not support front-door
identification , meaning that certain special cases, which are in
principle identifiable, are currently not handled.
We conclude with a brief look at the possible extensions of the package, which we hope to consider in future work:
Spurious pathways: the package allows for correcting discrimination along causal pathways from the protected attribute A to outcome Y. However, it is also possible that A and Y are associated through a confounding mechanism, and correcting for such spurious association poses an interesting methodological and practical challenge.
General identification: as discussed above, there are
certain cases of causal graphs in which our do(A = a, R = R(a′))
intervention is identifiable, but the fairadapt()
function
currently does not support doing so. One of such examples is front-door
identification, mentioned above. In a future version of , we hope to
cover all scenarios in which identification is possible.
Path-specific effects: when using resolving variables (Section ), the user decides to label these variables as “non-discriminatory”, that is, the algorithm is free to distinguish between groups based on these variables. In full generality, a user might be interested in considering all path-specific effects . Such an approach would offer even more flexibility in modeling, since for every attribute-outcome path A → ... → Y, the user could decide whether it is fair or not.
Selection bias: a commonly considered problem in causal inference is that of selection bias , when inclusion of individuals into the dataset depends on the observed variables in the dataset. In fairness applications, the presence of selection bias could invalidate our conclusions about discrimination and make our fair predictions biased. Recovering from selection bias algorithmically would therefore be a desirable feature in the package.
Non-binary attribute A: one assumption used currently is that the protected attribute A is binary. When considering possible protected attributes as socioeconomic status or education, this assumption may need to be relaxed. We hope to address this in future work.
This assumption of course is not empirically testable, as it is impossible to observe both a female and a male version of the same individual.↩︎