true

Introduction

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.

Definitions of fairness

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:

  1. Demographic parity , which requires the protected attribute A (gender, race, religion etc.) to be independent of a constructed classifier or regressor , written as ⊥   ⊥A.
  2. Equality of odds , which requires equal false positive and false negative rates of classifier between different groups (females and males for example) written as ⊥   ⊥A ∣ Y.
  3. Calibration , which requires the protected attribute to be independent of the actual outcome given the prediction Y⊥   ⊥A ∣ . Intuitively, this means that the protected attribute A does not offer additional information about the outcome Y once we know what the prediction is.

Fairness tasks

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.

Software options for fair data analysis in \proglang{R} and \proglang{python}. In the left column, nodes correspond to \proglang{R}-packages and in the right column to \proglang{python} modules. DP stands for demographic parity, EO equality of odds.

Software options for fair data analysis in and . In the left column, nodes correspond to -packages and in the right column to modules. DP stands for demographic parity, EO 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.

A causal approach

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.

Novelty in the package

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:

  • The methodology has been extended from the Markovian to the Semi-Markovian case (allowing noise variables to be correlated), which generalizes the scope of applications.
  • Backdoor paths into the protected attribute A are now allowed, meaning that the attribute A does not need to be a root node of the causal graph.
  • The user is provided with functionality for uncertainty quantification of the estimates.
  • Introduction of S3 classes fairadapt and fairadaptBoot, alongside associated methods, provides a more formalized implementation.
  • More flexibility is allowed in the quantile learning step, including different algorithms for quantile regression (linear, forest based, neural networks). Additionally, there is also a possibility of specifying a custom quantile learning function (utilizing S3 dispatch).
  • The user is provided with functionality for assessing the quality of the quantile regression fit, which allows for tuning the hyperparameters of the more flexible algorithms.

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.

Methodology

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 .

University admission example

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 .

A visual representation of the university admission example. $A$ denotes gender, $E$ previous educational achievement, and $T$ an admissions test score. The outcome $Y$ represents the final score used for the admission decision. The protected attribute $A$ has a discriminatory causal effect on variables $E$, $T$, and $Y$, which we wish to remove.

A visual representation of the university admission example. A denotes gender, E previous educational achievement, and T an admissions test score. The outcome Y represents the final score used for the admission decision. The protected attribute A has a discriminatory causal effect on variables E, T, and Y, which we wish to remove.

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 .

Structural causal models

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

  • V = {V1, …, Vn} is the set of observed (endogenous) variables.
  • U = {U1, …, Un} are latent (exogenous) variables.
  • ℱ = {f1, …, fn} is the set of functions determining V, vi ← fi(pa(vi), ui), where pa(Vi) ⊂ V, Ui ⊂ U are the functional arguments of fi and pa(Vi) denotes the parent vertices of Vi.
  • $\mathbbm{P}(u)$ is a distribution over the exogenous variables U.

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

  1. fi(pa(vi), ui) is increasing in ui for every fixed pa(vi).
  2. Exogenous variables Ui are uniformly distributed on [0, 1].

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 ).

Markovian SCM formulation

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 .

Implementation

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:

  1. Attribute A: the protected attribute is assumed to be binary. The column in can be of any data type coercible to a , but can only take two distinct values. Otherwise an error is thrown.
  2. Outcome Y: the dependent variable specified on the left hand side of the argument can be either a , , (treated the same as an ordered factor), , or a .
  3. Remaining covariates X: all other variables do not have limitations. Unordered or inputs can also be used as covariates X.

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:

summary(basic)
## 
## 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:

head(adaptedData(basic, train = FALSE))
##     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

Specifying the graphical model

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.

uni_graph <- graphModel(uni_adj)
The underlying graphical model corresponding to the university admission example (also shown in Figure \ref{fig:uni-adm}).

The underlying graphical model corresponding to the university admission example (also shown in Figure ).

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.

Quantile learning step

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.

Quantile methods

The quantile learning step of Algorithm can in principle be carried out by several methods, three of which are implemented in :

  • Quantile Regression Forests .
  • Non-Crossing Quantile Neural Networks .
  • Linear Quantile Regression .

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.

Influencing the fit

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.

Computational speed

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).

Fit quality

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:

  1. for 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),
  2. for 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).

Extending to custom methods

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:

  1. A 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.
  2. A logical flag, indicating whether the protected attribute is the root node of the causal graph. If the attribute A is a root node, we know that $$\mathbbm{P}(X \mid \text{do}(A = a)) = \mathbbm{P}(X \mid A = a).$$ Therefore, the interventional and conditional distributions are in this case the same, and we can leverage this knowledge in the quantile learning procedure, by splitting the data into A = 0 and A = 1 groups.
  3. A 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:

  1. Infer the quantile of vi ∣ pa(vi).
  2. Compute the counterfactual value vi(fp) under the change of protected attribute, using the counterfactual values of parents pa(vi(fp)) computed in previous steps (values pa(vi(fp)) are contained in the newdata argument).

For an example, see the computeQuants.ranger() method for a ranger object, which can be invoked by the computeQuants() generic.

Fair-twin inspection

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:

ft_basic <- fairTwins(basic, train.id = seq_len(n_samp))
head(ft_basic, n = 3)
##   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).

Uncertainty quantification

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 ).

The typical workflow when using \pkg{fairadapt}. The shaded region represents the fair pre-processing which happens within the \pkg{fairadapt} package. Often, this is followed by applying a regressor or a classifier to the transformed data, in order to obtain fair predictions. The latter part is up to the user and not included in \pkg{fairadapt}.

The typical workflow when using . The shaded region represents the fair pre-processing which happens within the package. Often, this is followed by applying a regressor or a classifier to the transformed data, in order to obtain fair predictions. The latter part is up to the user and not included in .

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 .

The causal graph for the COMPAS dataset. $Z$ are demographic features, $A$ is race, $J$ juvenile offense counts, $P$ priors count, $D$ the degree of charge, and $Y$ two year recidivism.

The causal graph for the COMPAS dataset. Z are demographic features, A is race, J juvenile offense counts, P priors count, D the degree of charge, and Y two year recidivism.

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))

Analyzing the uncertainty

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.

Decision-maker analysis

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()`).
Analyzing uncertainty of predictions in the COMPAS dataset from decision-maker's point of view. Panel A shows how the Jaccard similarity of two repetitions varies depending on the decision threshold. Panel B shows the cumulative distribution of the random variable that indicates whether two randomly selected individuals preserve order (in terms of predicted probabilities) in bootstrap repetitions. Panel C shows the density of the normalized inversion number of between predicted probabilities in bootstrap repetitions. Panel D shows the cumulative distribution function of the 95\% confidence interval (CI) width for the predicted probability of different individuals.

Analyzing uncertainty of predictions in the COMPAS dataset from decision-maker’s point of view. Panel A shows how the Jaccard similarity of two repetitions varies depending on the decision threshold. Panel B shows the cumulative distribution of the random variable that indicates whether two randomly selected individuals preserve order (in terms of predicted probabilities) in bootstrap repetitions. Panel C shows the density of the normalized inversion number of between predicted probabilities in bootstrap repetitions. Panel D shows the cumulative distribution function of the 95% confidence interval (CI) width for the predicted probability of different individuals.

Individual analysis

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.

Analyzing the spread of individual predictions in the COMPAS dataset, resulting from different bootstrap repetitions.

Analyzing the spread of individual predictions in the COMPAS dataset, resulting from different bootstrap repetitions.

Illustration

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:

The causal graph for the government-census dataset. $D$ are demographic features, $A$ is gender, $F$ represents marital and family information, $E$ education, $W$ work-related information and $Y$ the salary, which is also the outcome of interest. The bidirected edge between $A$ and $D$ represents that the noise variables $U_A$ and $U_D$ share information.

The causal graph for the government-census dataset. D are demographic features, A is gender, F represents marital and family information, E education, W work-related information and Y the salary, which is also the outcome of interest. The bidirected edge between A and D represents that the noise variables UA and UD share information.

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.
Full causal graph for the government census dataset, expanding the grouped view presented in Figure \ref{fig:census-tikz}. \textit{Demographic} features include age (\textbf{ag}), race (\textbf{ra}), whether an employee is of Hispanic origin (\textbf{hi}), is US citizen (\textbf{ci}), whether the citizenship is native (\textbf{na}), alongside the corresponding economic region (\textbf{ec}). \textit{Familial} features are marital status (\textbf{ma}), family size (\textbf{fa}) and number of children (\textbf{ch}), \textit{educational} features include education (\textbf{ed}) and English language levels (\textbf{en}), and \textit{occupational} features, weekly working hours (\textbf{ho}), yearly working weeks (\textbf{we}), job (\textbf{oc}) and industry identifiers (\textbf{in}). Finally, the yearly salary (\textbf{sa}) is used as the \textit{response} variable and employee sex (\textbf{se}) as the \textit{protected} attribute variable.

Full causal graph for the government census dataset, expanding the grouped view presented in Figure . features include age (), race (), whether an employee is of Hispanic origin (), is US citizen (), whether the citizenship is native (), alongside the corresponding economic region (). features are marital status (), family size () and number of children (), features include education () and English language levels (), and features, weekly working hours (), yearly working weeks (), job () and industry identifiers (). Finally, the yearly salary () is used as the variable and employee sex () as the attribute variable.

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.

n_samp <- 750
n_pred <- 5
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`.
Visualization of salary densities grouped by employee sex, before (panel A) and after adaptation (panel B). Panel A indicates a shift towards higher values for male employees. In panel B, after the data is transformed, the gap between groups is reduced.

Visualization of salary densities grouped by employee sex, before (panel A) and after adaptation (panel B). Panel A indicates a shift towards higher values for male employees. In panel B, after the data is transformed, the gap between groups is reduced.

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:

fairTwins(gov_ada, train.id = 1:5,
          cols = c("sex", "age", "salary"))
##      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.

Extensions

Several extensions to the basic Markovian SCM formulation introduced in Section exist, and these are outlined in the following sections.

Adding resolving variables

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

  1. R ∈ de(A), where de(A) are the descendants of A in the causal graph 𝒢.
  2. The causal effect of A on R is considered to be non-discriminatory.

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.

uni_res <- graphModel(uni_adj, res.vars = "test")
Visualization of the causal graph corresponding to the university admissions example introduced in Section \ref{introduction} with the variable \texttt{test} chosen as a \textit{resolving variable} and therefore highlighted in red.

Visualization of the causal graph corresponding to the university admissions example introduced in Section with the variable chosen as a and therefore highlighted in red.

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.

Semi-Markovian and topological ordering variant

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 .

Causal graphical model corresponding to a Semi-Markovian variant of the university admissions example, introduced in Section \ref{implementation}. Here, we allow for the possibility of a mutual dependency between the latent variables corresponding to variables `test` and `score`.

Causal graphical model corresponding to a Semi-Markovian variant of the university admissions example, introduced in Section . Here, we allow for the possibility of a mutual dependency between the latent variables corresponding to variables test and score.

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.
  • As with the adjacency matrix 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")
Visualization of the causal graphical model also shown in Figure \ref{fig:semi-markov}, obtained when passing a confounding matrix indicating a bidirected edge between vertices \texttt{test} and \texttt{score} to \texttt{fairadapt()}. The resulting Semi-Markovian model can also be handled by \texttt{fairadapt()}, extending the basic Markovian formulation introduced in Section \ref{markovian-scm-formulation}.

Visualization of the causal graphical model also shown in Figure , obtained when passing a confounding matrix indicating a bidirected edge between vertices and to . The resulting Semi-Markovian model can also be handled by , extending the basic Markovian formulation introduced in Section .

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.

Questions of identifiability

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:

  • The model is Markovian.
  • The model is Semi-Markovian and,
    1. there is no bidirected path between A and ch(A) and,
    2. there is no bidirected path between Ri and ch(Ri) for any resolving variable Ri.

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.

Future avenues to be explored

We conclude with a brief look at the possible extensions of the package, which we hope to consider in future work:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.


  1. 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.↩︎