| Title: | Fair Gated Algorithm for Targeted Equity |
| Version: | 1.0.0 |
| Description: | Fairness-aware correction of a prediction model for binary and continuous outcomes with two or more protected groups and their intersections. A cross-fitted base learner (gradient-boosted trees or a neural network) is fitted without the protected attribute, and a gated mixture of neural experts learns a per-person correction to its score under a fairness penalty (a random-effects or variance penalty on outcome-stratified group means, or a thresholded worst-group penalty on error rates), with a cross-fitted disparity signal, an intervention cost, smoothed early stopping, a per-person export of adjustments with bootstrap stability intervals, and routing analyses of which expert corrects whom. Methods draw on mixture-of-experts neural networks by Jordan and Jacobs (1994) <doi:10.1007/978-1-4471-2097-1_113>, gradient boosting by Chen and Guestrin (2016) <doi:10.1145/2939672.2939785>, cross-fitting by Chernozhukov and co-authors (2018) <doi:10.1111/ectj.12097>, equalised odds by Hardt, Price and Srebro (2016) <doi:10.48550/arXiv.1610.02413>, the random-effects variance estimator of DerSimonian and Laird (1986) <doi:10.1016/0197-2456(86)90046-2>, and personalised treatment prediction for depression by Iniesta, Stahl and McGuffin (2016) <doi:10.1016/j.jpsychires.2016.03.016>. |
| License: | MIT + file LICENSE |
| Encoding: | UTF-8 |
| Language: | en-GB |
| Depends: | R (≥ 4.1.0) |
| Imports: | dplyr, ggalluvial, ggplot2, magrittr, pROC, purrr, readr, rlang, stats, tibble, tidyr, tidyselect, utils |
| Suggests: | knitr, rmarkdown, testthat (≥ 3.0.0), torch, xgboost (≥ 3.1.0) |
| Config/testthat/edition: | 3 |
| VignetteBuilder: | knitr |
| URL: | https://github.com/rhysholland/FairGATE |
| BugReports: | https://github.com/rhysholland/FairGATE/issues |
| SystemRequirements: | Optional 'LibTorch' backend for the neural-network components; install via torch::install_torch(). |
| LazyData: | true |
| Config/roxygen2/version: | 8.1.0 |
| NeedsCompilation: | no |
| Packaged: | 2026-10-26 10:26:04 UTC; k2584926 |
| Author: | Rhys Holland [aut, cre], Raquel Iniesta [aut] |
| Maintainer: | Rhys Holland <rhys.holland@icloud.com> |
| Repository: | CRAN |
| Date/Publication: | 2026-10-26 13:10:07 UTC |
fairGATE: Fair Gated Algorithm for Targeted Equity
Description
fairGATE corrects an independently fitted prediction model for fairness
across protected groups. A cross-fitted base learner (XGBoost or a neural
network, fairgate_base_learner()) is fitted without the protected
attribute; a gated mixture of neural experts (train_gnn()) then learns a
per-person correction to its score under a fairness penalty, for binary or
continuous outcomes and two or more groups. fairgate() runs the whole
method, including the configuration search and selection rule of the
accompanying study, in one call, and fairgate_export() returns the
per-person adjustments with their stability intervals.
Details
The neural-network components use the 'torch' package, which needs the
LibTorch backend (torch::install_torch()). For reproducible fits call
torch::torch_set_num_threads(1) first.
Author(s)
Maintainer: Rhys Holland rhys.holland@icloud.com
Authors:
Rhys Holland rhys.holland@icloud.com
Raquel Iniesta raquel.iniesta@kcl.ac.uk
See Also
Useful links:
Report bugs at https://github.com/rhysholland/FairGATE/issues
Adult ready (small sample)
Description
A compact (n = 1000) sample of the pre-cleaned Adult dataset for examples and vignettes. All original columns are retained; only rows are subsampled.
Usage
adult_ready_small
Format
A data frame with 1000 rows and K columns (same schema as the full dataset).
Details
Created from the full CSV by random sampling with a fixed seed:
adult_ready_small <- dplyr::slice_sample(big, n = 1000).
Source
UCI Adult (pre-cleaned in-package).
Examples
data("adult_ready_small", package = "fairGATE")
str(adult_ready_small)
head(adult_ready_small)
Analyse and Visualise Expert Network Specialisation
Description
Analyses expert input weights to determine which features are most important per subgroup. For two groups, returns a signed difference plot (GroupB - GroupA). For >2 groups, performs a non-inferential analysis: per-group mean importances, pairwise (B - A) difference tables for all pairs, and a multi-group plot for the features with the largest max-min spread across groups.
Usage
analyse_experts(
gnn_results,
prepared_data,
group_mappings = NULL,
top_n_features = 10,
verbose = FALSE
)
Arguments
gnn_results |
A list from |
prepared_data |
A list from |
group_mappings |
Optional named mapping from group codes (names) to labels (values). |
top_n_features |
Integer; number of top features to visualise. |
verbose |
Logical; print progress messages. |
Value
A list with:
all_weights |
Long table of feature importances by group & repeat |
means_by_group_wide |
Wide table of per-feature mean importance per group |
pairwise_differences |
Named list of B_vs_A difference tables (descriptive) |
difference_plot |
ggplot; only when there are exactly 2 groups |
multi_group_plot |
ggplot; only when there are >2 groups |
top_features_multi |
Long table used for the multi-group plot |
Examples
if (requireNamespace("torch", quietly = TRUE) && torch::torch_is_installed()) {
data("adult_ready_small", package = "fairGATE")
d <- adult_ready_small
# drop columns with missing values or no variation before scaling
keep <- vapply(d, function(v) !anyNA(v) && (!is.numeric(v) || stats::sd(v) > 0), TRUE)
pd <- prepare_data(d[, keep], outcome_var = "income", group_var = "sex")
fit <- train_gnn(pd, run_tuning = FALSE,
best_params = list(lr = 0.01, hidden_dim = 8, dropout_rate = 0.1,
lambda = 1, temperature = 1),
num_repeats = 1, epochs = 5, seed = 1)
ex <- analyse_experts(fit, pd, top_n_features = 5)
head(ex$means_by_group_wide)
}
Analyse and Visualise GNN Results
Description
Generates ROC/Calibration outputs and subgroup gate analyses.
Usage
analyse_gnn_results(
gnn_results,
prepared_data,
group_mappings = NULL,
create_roc_plot = TRUE,
create_calibration_plot = TRUE,
analyse_gate_weights = TRUE,
analyse_gate_entropy = TRUE,
verbose = FALSE,
nonparametric = FALSE
)
Arguments
gnn_results |
List from |
prepared_data |
List from |
group_mappings |
Optional named mapping from group codes (names) to labels (values). |
create_roc_plot |
Logical; return ROC ggplot (default TRUE). |
create_calibration_plot |
Logical; return calibration ggplot (default TRUE). |
analyse_gate_weights |
Logical; analyse gate weights across groups (default TRUE). |
analyse_gate_entropy |
Logical; analyse gate entropy across groups (default TRUE). |
verbose |
Logical; print progress messages. |
nonparametric |
Logical; if TRUE and k>2, use Kruskal-Wallis + Wilcoxon instead of ANOVA + Tukey. |
Value
A list of ggplots, test results, and summary tables.
Examples
if (requireNamespace("torch", quietly = TRUE) && torch::torch_is_installed()) {
data("adult_ready_small", package = "fairGATE")
d <- adult_ready_small
# drop columns with missing values or no variation before scaling
keep <- vapply(d, function(v) !anyNA(v) && (!is.numeric(v) || stats::sd(v) > 0), TRUE)
pd <- prepare_data(d[, keep], outcome_var = "income", group_var = "sex")
fit <- train_gnn(pd, run_tuning = FALSE,
best_params = list(lr = 0.01, hidden_dim = 8, dropout_rate = 0.1,
lambda = 1, temperature = 1),
num_repeats = 1, epochs = 5, seed = 1)
res <- analyse_gnn_results(fit, pd)
names(res)
}
Export predictions for IBM Fairness 360
Description
Create a CSV (or return a data frame) with columns expected by common IBM Fairness 360 workflows: subjectid, y_true, y_pred, score, group, group_label, plus optional gate columns.
Usage
export_f360_csv(
gnn_results,
prepared_data,
path = NULL,
include_gate_cols = TRUE,
threshold = 0.5,
verbose = FALSE
)
Arguments
gnn_results |
List returned by train_gnn(); must contain $final_results. |
prepared_data |
List returned by prepare_data(); used to pull group mappings. |
path |
Optional file path to write the CSV. If NULL, no file is written. |
include_gate_cols |
Logical; include gate_prob_expert_* and gate_entropy if available. Default TRUE. |
threshold |
Numeric between 0 and 1 inclusive; classification threshold for y_pred. Default 0.5. |
verbose |
Logical; if TRUE, prints progress messages. Default FALSE. |
Value
Invisibly returns the data.frame that is written (or would be written).
Examples
## Not run:
tmp <- file.path(tempdir(), "fairness360_input.csv")
export_f360_csv(
gnn_results = final_fit,
prepared_data = prepared_data_chd,
path = tmp,
include_gate_cols = TRUE,
threshold = 0.5,
verbose = TRUE
)
## End(Not run)
Fit fairGATE in One Call
Description
Fits the cross-fitted base learner (XGBoost or a neural network), the cross-fitted disparity signal, searches the fairGATE configuration space on an inner split under the selection rule of the accompanying study, and fits the selected configuration on every partition with per-person adjustment intervals. The defaults reproduce the fairGATE protocol of the study.
Usage
fairgate(
X,
y = NULL,
group = NULL,
family = c("binomial", "gaussian"),
reference_group = NULL,
splits = NULL,
n_partitions = 1L,
test_prop = 0.2,
inner = NULL,
base_learner = c("xgboost", "nn"),
base_params = NULL,
base_tune_args = NULL,
base_score = NULL,
signal = NULL,
signal_learner = c("xgboost", "nn"),
signal_params = NULL,
gate_signal = NULL,
params = NULL,
space = fairgate_space(),
n_draws = space$n_draws %||% 48L,
perf_tolerance = 0.01,
crossed_groups = NULL,
epochs = 300L,
es_prop = 0.15,
es_patience = 30L,
interval_reps = 10L,
interval_epochs = 10L,
seed = 4242L,
nthread = 1L,
verbose = FALSE
)
Arguments
X |
Numeric feature matrix or data frame, without the protected
attribute; or the list returned by |
y |
Outcome: 0/1 ( |
group |
Integer codes of the protected attribute. Used only in the fairness objective, in the target of the disparity signal and in evaluation; never as an input to the base learner, the gate or the experts. |
family |
|
reference_group |
Code of the reference (privileged) group. Defaults to the largest group. |
splits |
Optional list of partitions, each |
n_partitions, test_prop |
Number of partitions and held-out share used
when |
inner |
Optional |
base_learner |
|
base_params |
Optional fixed base-learner hyperparameters (see
|
base_tune_args |
Optional list of further arguments to
|
base_score |
Optional precomputed base scores: a list with element
|
signal |
Logical; use the cross-fitted disparity signal in the gate.
Defaults to the |
signal_learner |
Learner for the disparity signal, |
signal_params |
Hyperparameters of the signal learner; default 200 rounds, learning rate 0.05, depth 4, minimum child weight 5, subsample and column subsample 0.8, L2 penalty 1. |
gate_signal |
Optional precomputed signal in the layout of
|
params |
Optional fixed fairGATE configuration (a named list with the
elements of |
space |
Search space and fixed settings, default |
n_draws |
Number of configurations searched. Default 48. |
perf_tolerance |
Selection rule: among configurations whose inner-validation AUC or R-squared is within this distance of the best, the one with the smallest worst-group absolute disparity (average odds difference at the prevalence-matched threshold, or conditional mean prediction difference in outcome standard deviations). Default 0.01. |
crossed_groups |
Optional two-column matrix of two protected
attributes for the crossed penalty (used only when |
epochs, es_prop, es_patience |
Epoch cap, early-stopping slice and
patience of |
interval_reps, interval_epochs |
Bootstrap replicates and epochs per
replicate of the per-person adjustment intervals. Defaults 10 and 10;
|
seed |
Integer seed. Configuration |
nthread |
Threads used by XGBoost. Default 1. |
verbose |
Logical; print progress. |
Details
Base learner. Fitted without the protected attribute by
fairgate_base_learner(): 5-fold out-of-fold scores for training rows, a
fit on all training rows for held-out rows.
Disparity signal. For training rows a target is computed by
fairgate_disparity_target() from the outcome, the protected group and
the out-of-fold base score; a regression on the features and the
link-scale base score predicts it, cross-fitted in the same way, and the
prediction is given to the gate only.
Search and selection. n_draws configurations drawn by
fairgate_draw_configs() are fitted on inner and scored on its
validation rows; the selection rule is described under perf_tolerance,
with the worst group taken over groups passing a minimum-cell rule (at
least 10 expected positives and negatives, or 50 expected people, per
held-out set, computed as full-sample counts times test_prop). When no
group passes the rule (small samples), the worst group is taken over all
non-reference groups, with a warning.
Final fit. The selected configuration is fitted on every partition
by train_gnn() with median imputation, standardisation, early stopping
on a stratified 15 per cent slice (smoothed over 5 epochs, earliest
selectable epoch 10, patience 30, cap 300) and per-person adjustment
intervals from warm-started bootstrap replicates.
The full protocol is computationally heavy (48 searched configurations of
up to 300 epochs each). For a quick look reduce n_draws, epochs and
interval_reps.
Value
An object of class fairgate_fit: a list with predictions (a
data frame with partition, row, y, group, base, pred and
adjustment for every held-out row), export (the per-person export:
the routing weights, base weight, adjustment and, when intervals are on,
adj_lo, adj_hi, adj_sd and interval_excludes_zero), selected
(the selected configuration with its fixed settings), search (one row
per searched configuration with inner-validation performance and
worst-group disparity), base (base-learner scores and settings),
signal, fit (the train_gnn() result) and settings.
Examples
if (requireNamespace("torch", quietly = TRUE) && torch::torch_is_installed() &&
requireNamespace("xgboost", quietly = TRUE) &&
utils::packageVersion("xgboost") >= "3.1.0") {
set.seed(1)
n <- 800
g <- rbinom(n, 1, 0.3)
X <- cbind(x1 = rnorm(n) + 0.5 * g, x2 = rnorm(n), x3 = rnorm(n))
y <- rbinom(n, 1, stats::plogis(X[, 1] - 0.5 * X[, 2]))
fg <- fairgate(X, y, g, "binomial", reference_group = 0,
base_params = list(nrounds = 50, max_depth = 3),
n_draws = 2, epochs = 20, interval_reps = 2,
interval_epochs = 2)
fg
head(fg$export)
}
Default Hyperparameter Grids for the Base Learner
Description
Returns the candidate values searched by fairgate_tune_base_learner() for
each base learner. Every element is a vector of candidate values; a single
value is held fixed.
Usage
fairgate_base_grid(base_learner = c("xgboost", "nn"))
Arguments
base_learner |
|
Details
XGBoost ("xgboost", gradient-boosted trees with the histogram method):
max_depthtree depth, 3, 4, 6 or 8
etalearning rate, 0.02, 0.05 or 0.1
nroundsceiling on boosting rounds, 1000; the number actually used is chosen by early stopping on the inner validation slice
early_stopping_rounds50 rounds without improvement in validation loss
subsamplerow subsampling per tree, 0.7, 0.9 or 1
colsample_bytreecolumn subsampling per tree, 0.7, 0.9 or 1
min_child_weightminimum hessian weight per leaf, 1, 5 or 10
lambda,alphaL2 penalty 0.5, 1 or 5 and L1 penalty 0 or 0.5 on leaf weights
These are the values of the gradient-boosting comparator in the accompanying study, with the number of rounds chosen by early stopping rather than drawn from a list.
Neural network ("nn", a torch multilayer perceptron with rectified
linear units and dropout, trained by Adam on mini-batches):
n_layershidden layers, 1, 2 or 3
widthunits per hidden layer, 32, 64, 128 or 256
dropoutdropout rate, 0, 0.1, 0.2 or 0.3
lrAdam learning rate, 3e-4, 1e-3 or 3e-3
weight_decayAdam weight decay, 0, 1e-5, 1e-4 or 1e-3
batch_sizemini-batch size, 256, 512 or 1024
epochsceiling on epochs, 200; the number actually used is chosen by early stopping on the inner validation slice
patience20 epochs without improvement in validation loss
The loss is binary cross-entropy on the logit (binary outcome) or squared error on the standardised outcome (continuous outcome).
Value
A named list of candidate values.
Examples
str(fairgate_base_grid("xgboost"))
str(fairgate_base_grid("nn"))
Cross-Fitted Base Learner (XGBoost or Neural Network)
Description
Fits the base learner whose scores fairGATE corrects. Training rows receive
out-of-fold scores from n_folds-fold cross-fitting, so that no training
row is scored by a model that saw its outcome; held-out rows receive the
prediction of a fit on all training rows. Features are median-imputed and
standardised with the training rows' constants. The protected attribute is
never an input: pass only the features in X.
Usage
fairgate_base_learner(
X,
y,
family = c("binomial", "gaussian"),
train = NULL,
test = NULL,
base_learner = c("xgboost", "nn"),
params = NULL,
n_folds = 5L,
folds = NULL,
seed = 4242L,
standardise = TRUE,
nthread = 1L,
tune_args = NULL,
verbose = FALSE
)
Arguments
X |
Numeric feature matrix or data frame. It must not contain the protected attribute. |
y |
Outcome: 0/1 for |
family |
|
train |
Integer row indices of the training rows (default all rows). |
test |
Optional integer row indices of held-out rows to score. |
base_learner |
|
params |
Named list of fixed hyperparameters. When |
n_folds |
Number of cross-fitting folds. Default 5. |
folds |
Optional integer vector of fold labels |
seed |
Integer seed for the split, the random draws and the model fits. The caller's random number state is restored on exit. |
standardise |
Logical; centre and scale features with the inner training rows' constants (median imputation of missing values first). |
nthread |
Threads used by XGBoost. Default 1 (reproducible). |
tune_args |
Optional list of further arguments to
|
verbose |
Logical; print one line per configuration. |
Value
An object of class fairgate_base: a list with train and test
(scores on the response scale, probabilities for a binary outcome, in the
order of train and test), train_link and test_link (the same on
the link scale: logit, or standardised by the training outcome mean and
standard deviation), train_rows, test_rows, folds, params,
tuning (the tuning result or NULL), perf (out-of-fold and, when
test is given, held-out AUC or R-squared), family and
base_learner. Pass list(list(train = b$train, test = b$test)) as
base_score to train_gnn().
Examples
if (requireNamespace("xgboost", quietly = TRUE) &&
utils::packageVersion("xgboost") >= "3.1.0") {
set.seed(2)
X <- matrix(rnorm(500 * 4), 500, 4)
y <- X[, 1] + 0.5 * X[, 2] + rnorm(500)
b <- fairgate_base_learner(X, y, "gaussian", train = 1:400, test = 401:500,
base_learner = "xgboost",
params = list(nrounds = 50, max_depth = 3))
b$perf
}
if (requireNamespace("torch", quietly = TRUE) && torch::torch_is_installed()) {
set.seed(3)
X <- matrix(rnorm(400 * 4), 400, 4)
y <- rbinom(400, 1, stats::plogis(X[, 1]))
b <- fairgate_base_learner(X, y, "binomial", train = 1:300, test = 301:400,
base_learner = "nn",
params = list(epochs = 10, width = 16))
b$perf
}
Crossed Variance-Component Penalty for Intersectional Groups
Description
Decomposes, within each outcome stratum, the between-cell dispersion of the mean predictions of the (a, b) intersectional cells into two main effects and an interaction, each estimated with a correction for the sampling noise in the cell means, and returns their sum over strata.
Usage
fairgate_crossed_penalty(
pred,
plan,
penalty = c("random_effects", "bias_corrected", "variance"),
eps = 1e-12
)
Arguments
pred |
Predictions (tensor), on the penalty scale. |
plan |
From |
penalty |
|
eps |
Floor on sampling variances. |
Details
For stratum q write m_ab for the cell means and v_ab for their
detached sampling variances. The main effect of A is the between-level
variance component (as in fairgate_penalty_from_cells()) of the
unweighted marginal means m_a. = mean_b m_ab, with sampling variances
sum_b v_ab / B_a^2; likewise for B. The interaction is computed on the
largest complete sub-grid A' x B': with residuals
r_ab = m_ab - m_a. - m_.b + m_.., E(sum r^2) = (A'-1)(B'-1) sigma^2_int +
(1 - 1/A')(1 - 1/B') sum v_ab, because the residual projection has constant
diagonal, so sigma^2_int is estimated by
max(0, (sum r^2 - (1 - 1/A')(1 - 1/B') sum v) / ((A'-1)(B'-1))). With a
single level of B the penalty reduces to the one-way component of A.
Value
A scalar tensor.
Examples
if (requireNamespace("torch", quietly = TRUE) && torch::torch_is_installed()) {
set.seed(1)
A <- sample(0:1, 400, replace = TRUE); B <- sample(0:2, 400, replace = TRUE)
strata <- sample(1:2, 400, replace = TRUE)
pred <- torch::torch_tensor(stats::plogis(rnorm(400) + 0.3 * A * B))
plan <- fairgate_crossed_plan(A, B, strata)
fairgate_crossed_penalty(pred, plan, penalty = "random_effects")
}
Cell Structure for Crossed (Intersectional) Variance Components
Description
Cell Structure for Crossed (Intersectional) Variance Components
Usage
fairgate_crossed_plan(
A,
B,
strata,
Q = max(as.integer(strata)),
min_cell = 2L,
dtype = torch::torch_float()
)
Arguments
A, B |
Codes of the two protected attributes, one per row. |
strata |
Outcome stratum identifiers in |
Q |
Number of strata. |
min_cell |
Smallest (a, b, stratum) cell that may contribute. |
dtype |
torch dtype. |
Value
A list with one element per stratum holding the indicator matrix of the contributing (a, b) cells, their sizes and codes.
Examples
if (requireNamespace("torch", quietly = TRUE) && torch::torch_is_installed()) {
set.seed(1)
A <- sample(0:1, 400, replace = TRUE); B <- sample(0:2, 400, replace = TRUE)
strata <- sample(1:2, 400, replace = TRUE)
plan <- fairgate_crossed_plan(A, B, strata)
length(plan)
}
Per-Person Disparity-Contribution Target
Description
The quantity whose cross-fitted prediction fairGATE feeds to the gate: for each training individual, the signed direction in which moving that individual's score would close its group's gap relative to the reference group, times its leverage on the decision.
Usage
fairgate_disparity_target(
y,
group,
base,
reference,
family = c("binomial", "gaussian"),
prevalence = NULL,
n_strata = 5L,
bandwidth = 1
)
Arguments
y |
Outcome. |
group |
Protected group codes. |
base |
Base-learner scores on the response scale, out of fold. |
reference |
Reference group code. |
family |
|
prevalence |
Share flagged positive (binomial); defaults to |
n_strata |
Outcome strata for |
bandwidth |
Bandwidth, on the logit scale, of the Gaussian leverage kernel centred on the threshold (binomial). |
Details
Binomial: with t the prevalence-matched threshold of base and
gaps d_kc = r_kc - r_ref,c in the rate of positive decisions of group k in
outcome class c, the target is -d_{g_i y_i} * phi((logit b_i - logit t)/h)
for a non-reference individual and +sum_k w_k d_{k y_i} * phi(.) for a
reference one, w_k the non-reference group shares. Gaussian: the same with
the stratum-conditional mean base prediction gap, in outcome standard
deviations, and unit leverage. The target uses the outcome and the
protected attribute, so it must be cross-fitted from the features alone
before it is given to the gate; the protected attribute itself never is.
Value
A numeric vector, one value per row.
Examples
set.seed(1)
n <- 400
g <- sample(0:2, n, replace = TRUE)
y <- rbinom(n, 1, 0.4)
base <- stats::plogis(rnorm(n) + y - 0.3 * g) # out-of-fold base scores
tgt <- fairgate_disparity_target(y, g, base, reference = 0, family = "binomial")
tapply(tgt, list(g, y), mean)
Draw fairGATE Configurations
Description
Draws distinct random configurations from a search space with the generator settings of the accompanying study.
Usage
fairgate_draw_configs(space, n = 48L, seed = 4242L)
Arguments
space |
Named list of candidate values, as |
n |
Number of distinct configurations. |
seed |
Integer seed. |
Value
A list of configurations (named lists). With the defaults of
fairgate_space() this is the configuration list searched in the study.
Examples
g <- fairgate_draw_configs(fairgate_space()$space, 3)
str(g[[1]])
Per-Person Export of a fairGATE Fit
Description
Returns, and optionally writes, the per-person table of a fairgate() fit:
partition, row, group, outcome, base score, fairGATE prediction,
adjustment, base weight, routing weights and, when intervals were fitted,
the adjustment interval (adj_lo, adj_hi) and whether it excludes zero.
Usage
fairgate_export(x, file = NULL)
Arguments
x |
A |
file |
Optional path of a CSV file to write. Nothing is written when
|
Value
A data frame, invisibly when file is given.
Examples
# fg <- fairgate(...)
# tab <- fairgate_export(fg)
# fairgate_export(fg, file.path(tempdir(), "fairgate_export.csv"))
fairGATE Fairness Penalty from Individual Predictions
Description
Evaluates the total fairness penalty sum_q L_q on a vector of individual
predictions, on the scale on which the penalty is defined: probabilities for
a binary outcome, the response scale for a continuous one.
Usage
fairgate_fair_penalty(
pred,
group = NULL,
strata = NULL,
Q = NULL,
penalty = c("random_effects", "bias_corrected", "variance"),
eps = 1e-12,
plan = NULL
)
Arguments
pred |
Individual predictions: a |
group |
Protected group codes, one per row, or |
strata |
Outcome stratum identifiers in |
Q |
Number of outcome strata. Defaults to |
penalty |
One of |
eps |
Lower bound applied to the cell sampling variances before they are
inverted. Default |
plan |
Optional precomputed cell structure from
|
Details
Per cell, the mean prediction is differentiable and the sampling variance is
not: the within-cell variance is computed from detached predictions, so the
weights of the random-effects rung cannot be gamed by inflating within-cell
spread. Cells with fewer than two rows carry no estimable within-cell
variance and are dropped for every rung except "variance", which needs
none and admits cells of one row.
Value
A scalar of the same kind as pred (tensor in, tensor out), zero
when no stratum contributes.
Examples
if (requireNamespace("torch", quietly = TRUE) && torch::torch_is_installed()) {
set.seed(1)
group <- sample(0:2, 200, replace = TRUE)
y <- rbinom(200, 1, 0.4)
pred <- torch::torch_tensor(stats::plogis(rnorm(200) + 0.3 * group), requires_grad = TRUE)
pen <- fairgate_fair_penalty(pred, group, y + 1L, penalty = "random_effects")
pen$backward()
as.numeric(pen)
}
Gate Maps: softmax, sparsemax and entmax-1.5
Description
Maps each row of a matrix of gate logits to a probability vector. The sparse maps return exact zeros, so a gate built on them can place all of an individual's weight on a single component, in particular on the base learner, in which case the fairGATE adjustment for that individual is exactly zero.
Usage
fairgate_gate_map(z, type = c("softmax", "sparsemax", "entmax15"))
Arguments
z |
A two-dimensional |
type |
One of |
Value
A tensor of the same shape whose rows are non-negative and sum to one. Differentiable; the sparse maps use their closed-form Jacobians.
Examples
if (requireNamespace("torch", quietly = TRUE) && torch::torch_is_installed()) {
z <- torch::torch_tensor(matrix(c(2, 1, -1, 0.5, 0.4, 0.3), 2, byrow = TRUE))
fairgate_gate_map(z, "softmax")
fairgate_gate_map(z, "sparsemax")
fairgate_gate_map(z, "entmax15")
}
Multi-Group Separation Disparity: Per Group, Worst Group and Joint
Description
Computes, for every non-reference group, the average odds difference (binary, at a supplied threshold) or the conditional mean prediction difference (continuous, in outcome standard deviations) relative to the reference group, and summarises them.
Usage
fairgate_group_disparity(
y,
pred,
group,
reference,
family = c("binomial", "gaussian"),
threshold = NULL,
edges = NULL,
scale = NULL,
eligible = NULL
)
Arguments
y |
Outcome. |
pred |
Predictions (probabilities for a binary outcome, the outcome scale for a continuous one). |
group |
Protected group codes. |
reference |
Reference group code. |
family |
|
threshold |
Decision threshold (binomial). Use the prevalence-matched
threshold |
edges |
Stratum edges for the continuous outcome; default the
equal-frequency quintiles of |
scale |
Outcome standard deviation used to express CMPD. |
eligible |
Optional vector of group codes over which the worst-group
maximum and the joint summaries are taken (a minimum-cell rule). Groups
outside it are still reported in |
Value
A list: per_group (data frame of group, n, gap), worst (the
largest absolute gap), worst_group, signed_worst (the gap of that
group), joint_mean (size-weighted mean absolute gap), joint_pairwise
(largest absolute gap between any two groups, reference included) and
pooled (every non-reference group pooled against the reference).
Examples
set.seed(1)
n <- 600
g <- sample(0:2, n, replace = TRUE)
y <- rbinom(n, 1, 0.4)
pred <- stats::plogis(rnorm(n) + y - 0.4 * g)
thr <- stats::quantile(pred, 1 - mean(y), names = FALSE) # prevalence-matched
d <- fairgate_group_disparity(y, pred, g, reference = 0, family = "binomial",
threshold = thr)
d$per_group
d$worst
Worst-Group Conditional Mean Gap Penalty (continuous outcomes)
Description
The continuous counterpart of fairgate_threshold_penalty(): for each
non-reference group the squared mean over outcome strata of the difference
between that group's and the reference group's mean prediction (the squared
CMPD of that group), aggregated by the maximum or the mean.
Usage
fairgate_group_gap_penalty(
pred,
plan,
reference = NULL,
agg = c("max", "mean")
)
Arguments
pred |
Predictions on the scale the penalty is defined on. |
plan |
From |
reference |
Reference group code, or |
agg |
|
Value
A scalar tensor.
Examples
if (requireNamespace("torch", quietly = TRUE) && torch::torch_is_installed()) {
set.seed(1)
group <- sample(0:2, 300, replace = TRUE)
y <- rnorm(300)
strata <- as.integer(cut(y, stats::quantile(y, 0:5 / 5), include.lowest = TRUE))
pred <- torch::torch_tensor(y + 0.2 * group)
plan <- fairgate_penalty_plan(group, strata, min_cell = 1L)
fairgate_group_gap_penalty(pred, plan, reference = 0)
}
Frontier Ladder of Expert Fairness Weights
Description
The auxiliary fairness weights carried by the individual experts of the frontier-anchored model.
Usage
fairgate_lambda_ladder(K, lambda_max)
Arguments
K |
Number of experts, which is the number of protected groups. |
lambda_max |
Fairness weight of the most heavily constrained expert. |
Details
Expert 1 is the accuracy anchor and carries \lambda_1 = 0. The
remaining experts are spaced geometrically,
\lambda_k = \lambda_{\max} 10^{-(K - k)/(K - 1)} for k \ge 2, so
that \lambda_K = \lambda_{\max}. The experts therefore sit at
different points of the fairness and accuracy trade-off by construction,
rather than having to discover a division of labour from a symmetric
starting point that the gate gradient cannot leave.
The gate gradient is the reason the anchoring is structural rather than
cosmetic. With \hat y_i = \sum_k \pi_{ik} f_{ik} and
\pi_i = \mathrm{softmax}(z_i / \tau),
\partial \hat y_i / \partial z_{il} =
\tau^{-1} \pi_{il} (f_{il} - \hat y_i), so the gradient reaching a gate
logit is proportional to that expert's departure from the mixture. Identical
experts make it exactly zero: the gate cannot specialise and the experts
never differentiate.
Value
A numeric vector of length K, increasing, with first element zero
and last element lambda_max. Length-one input returns 0.
Examples
fairgate_lambda_ladder(K = 4, lambda_max = 16)
Between-Group Variance Components for the fairGATE Fairness Penalty
Description
Computes one stratum's contribution to the fairGATE fairness penalty from the group-conditional mean predictions of that stratum and their sampling variances. This is the lowest level of the penalty and is exported so that the algebra can be exercised directly on constructed inputs, independently of any model fit.
Usage
fairgate_penalty_from_cells(
mhat,
vhat = NULL,
penalty = c("random_effects", "bias_corrected", "variance"),
eps = 1e-12
)
Arguments
mhat |
Group-conditional mean predictions for one outcome stratum: a
|
vhat |
Sampling variances of the entries of |
penalty |
One of |
eps |
Lower bound applied to |
Details
Write the cell (k, q) for the individuals in protected group k
and outcome stratum q, of size n_{kq}, with mean prediction
\hat m_{kq}, within-cell sample variance s^2_{kq} and sampling
variance \hat v_{kq} = s^2_{kq} / n_{kq}. Let K_q be the number
of groups contributing to stratum q. The three rungs are
"variance"L_q = \mathrm{Var}_k(\hat m_{kq}), the unbiased sample variance over theK_qgroup means. This is the penalty of fairGATE 0.2.0 and earlier."bias_corrected"L_q = \max(0, \mathrm{Var}_k(\hat m_{kq}) - K_q^{-1} \sum_k \hat v_{kq})."random_effects"the DerSimonian and Laird between-group variance component. With
w_{kq} = 1 / \hat v_{kq},\bar m_{wq} = \sum_k w_{kq} \hat m_{kq} / \sum_k w_{kq},Q_q = \sum_k w_{kq} (\hat m_{kq} - \bar m_{wq})^2andC_q = \sum_k w_{kq} - (\sum_k w_{kq}^2) / (\sum_k w_{kq}),L_q = \max(0, (Q_q - (K_q - 1)) / C_q).
The correction terms follow from the bias of the sample variance as an
estimator of the between-group dispersion. Writing
\hat m_k = \mu_k + e_k with e_k independent, mean zero and
variance v_k, the unbiased sample variance S^2 over the K
estimates satisfies E[S^2] = S^2(\mu) + K^{-1} \sum_k v_k. The second
term is a floor that no improvement in fairness can remove: an optimiser can
only reduce it by distorting predictions.
vhat, and therefore the weights and the constant C_q, are always
detached from the autograd graph. This is load-bearing rather than an
optimisation. If they were differentiable, the optimiser could lower the
penalty by inflating the within-cell prediction variance, which raises
\hat v_{kq} and shrinks the weighted departure from the pooled mean
without making any group's mean prediction closer to any other's. Gradients
therefore flow only through mhat.
The rungs nest exactly. If every \hat v_{kq} equals a common v,
then \sum w = K/v, \sum w^2 = K/v^2 and C = (K-1)/v, while
Q = (K-1) S^2 / v, so the random-effects rung collapses to
\max(0, S^2 - v), which is the bias-corrected rung; and as
v \to 0 that collapses in turn to S^2, the 0.2.0 penalty.
Value
A scalar: a torch_tensor if mhat was a tensor, otherwise a
numeric value. Zero when fewer than two groups are supplied.
See Also
fairgate_fair_penalty() for the same quantity computed from
individual predictions, and train_gnn() for its use in fitting.
Examples
# Three groups' mean predictions in one outcome stratum, with their
# sampling variances
mhat <- c(0.30, 0.35, 0.42)
vhat <- c(0.002, 0.004, 0.010)
fairgate_penalty_from_cells(mhat, penalty = "variance")
fairgate_penalty_from_cells(mhat, vhat, penalty = "bias_corrected")
fairgate_penalty_from_cells(mhat, vhat, penalty = "random_effects")
Cell Structure for the fairGATE Fairness Penalty
Description
Builds the fixed part of the fairness penalty for one set of rows: which group-by-stratum cells contribute, and the indicator matrices and cell sizes needed to form the group-conditional means. The result depends only on the protected attribute and the outcome strata, so it is computed once per partition and reused at every epoch.
Usage
fairgate_penalty_plan(
group,
strata,
Q = max(as.integer(strata)),
min_cell = 2L,
dtype = torch::torch_float()
)
Arguments
group |
Integer or numeric vector of protected group codes, one per row. |
strata |
Integer vector of outcome stratum identifiers in |
Q |
Number of outcome strata. Defaults to |
min_cell |
Smallest cell size that may contribute. Cells smaller than
this are dropped. Use |
dtype |
A torch dtype for the indicator matrices. Defaults to
|
Details
A group contributes to stratum q only if its cell in that stratum has at
least min_cell rows, and a stratum contributes only if at least two groups
contribute to it. Strata are treated independently, so a group absent from
one stratum still contributes to the others. Note that the 0.2.0 penalty
used a stricter global rule in which a group had to be present in every
stratum to contribute to any of them; train_gnn(penalty = "variance")
retains that rule for exact backward compatibility.
Value
A list with one element per contributing stratum, each a list with
the stratum index q, the contributing group codes, the indicator matrix
M (rows by contributing groups), the cell sizes n, and K the number
of contributing groups. An empty list when no stratum contributes.
Examples
if (requireNamespace("torch", quietly = TRUE) && torch::torch_is_installed()) {
set.seed(1)
group <- sample(0:2, 60, replace = TRUE)
strata <- sample(1:2, 60, replace = TRUE)
plan <- fairgate_penalty_plan(group, strata)
length(plan)
}
fairGATE Search Space and Fixed Settings
Description
The configuration space searched by fairgate() and the settings held
fixed, as used in the accompanying study.
Usage
fairgate_space()
Value
A list with space (candidate values: learning rate lr, hidden
width hidden_dim, dropout_rate, gate temperature, fairness weight
lambda, penalty type, thresholded-penalty weight lambda_thr,
intervention_cost and coupling), fixed (gate, routing balance,
adversary, smoothing window and earliest selectable epoch of early
stopping, threshold temperature, target and aggregation, whether the
disparity signal and crossed groups are used), n_draws (48 distinct
random configurations) and seed (4242; draws use the Mersenne-Twister
generator with Inversion and Rejection sampling, lambda_thr is set to 0
when penalty = "thresholded", and duplicate draws are rejected).
Examples
str(fairgate_space()$space)
Differentiable Penalty on Thresholded Group Error Rates
Description
Replaces the hard decision 1[p_i >= t] by sigmoid((p_i - t) / s), with
t the prevalence-matched threshold of the current batch (the
1 - prevalence quantile of the predictions, treated as a constant) and s
a temperature, and penalises the worst group's gap in the resulting soft
true- and false-positive rates relative to the reference group.
Usage
fairgate_threshold_penalty(
pred,
plan,
prevalence,
temp = 0.05,
target = c("eo", "aod"),
agg = c("max", "mean")
)
Arguments
pred |
Predicted probabilities, a |
plan |
|
prevalence |
Share of rows to flag positive, in |
temp |
Temperature |
target |
|
agg |
|
Value
A scalar tensor. As temp goes to zero the soft rates converge to
the hard rates at threshold t.
Examples
if (requireNamespace("torch", quietly = TRUE) && torch::torch_is_installed()) {
set.seed(1)
group <- sample(0:2, 300, replace = TRUE)
y <- rbinom(300, 1, 0.4)
p <- torch::torch_tensor(stats::plogis(rnorm(300) + y + 0.4 * group))
plan <- fairgate_threshold_plan(group, y, reference = 0)
fairgate_threshold_penalty(p, plan, prevalence = mean(y))
}
Cell Structure for the Thresholded Error-Rate Penalty
Description
Cell Structure for the Thresholded Error-Rate Penalty
Usage
fairgate_threshold_plan(
group,
y,
reference = NULL,
min_cell = 2L,
dtype = torch::torch_float()
)
Arguments
group |
Protected group codes, one per row. |
y |
Binary outcome (0/1) for |
reference |
Code of the reference group. |
min_cell |
Smallest (group, outcome class) cell that may contribute. |
dtype |
torch dtype of the indicator matrices. |
Value
A list with indicator matrices M1, M0 (rows by groups) for the
positive and negative classes, their sizes, the group codes and the column
of the reference group (0 for the pooled comparison).
Examples
if (requireNamespace("torch", quietly = TRUE) && torch::torch_is_installed()) {
set.seed(1)
group <- sample(0:2, 100, replace = TRUE)
y <- rbinom(100, 1, 0.4)
plan <- fairgate_threshold_plan(group, y, reference = 0)
names(plan)
}
Tune the Base Learner on an Inner Split
Description
Evaluates candidate hyperparameters of the XGBoost or neural-network base learner on an inner training/validation split of the training rows, with early stopping on the validation slice, and selects the configuration with the smallest validation loss (log loss for a binary outcome, mean squared error for a continuous one). The AUC or R-squared is reported beside it.
Usage
fairgate_tune_base_learner(
X,
y,
family = c("binomial", "gaussian"),
train = NULL,
valid = NULL,
base_learner = c("xgboost", "nn"),
grid = NULL,
n_search = 24L,
search = c("random", "grid"),
inner_prop = 0.25,
seed = 4242L,
standardise = TRUE,
nthread = 1L,
verbose = FALSE
)
Arguments
X |
Numeric feature matrix or data frame. It must not contain the protected attribute. |
y |
Outcome: 0/1 for |
family |
|
train |
Integer row indices to tune on (default all rows). |
valid |
Optional integer row indices of the validation slice. When
|
base_learner |
|
grid |
Named list of candidate values; defaults to
|
n_search |
Number of configurations to evaluate by seeded random search; when the grid has no more points than this, every point is evaluated. |
search |
|
inner_prop |
Validation share when |
seed |
Integer seed for the split, the random draws and the model fits. The caller's random number state is restored on exit. |
standardise |
Logical; centre and scale features with the inner training rows' constants (median imputation of missing values first). |
nthread |
Threads used by XGBoost. Default 1 (reproducible). |
verbose |
Logical; print one line per configuration. |
Value
A list with best (the selected hyperparameters, with nrounds or
epochs set to the early-stopped value, ready to pass as params to
fairgate_base_learner()), results (a data frame with one row per
configuration: its hyperparameters, val_loss, val_perf (AUC or
R-squared), best_iteration, secs and selected), and inner (the
training and validation row indices used).
Examples
if (requireNamespace("xgboost", quietly = TRUE) &&
utils::packageVersion("xgboost") >= "3.1.0") {
set.seed(1)
X <- matrix(rnorm(600 * 5), 600, 5)
y <- rbinom(600, 1, stats::plogis(X[, 1] - X[, 2]))
tu <- fairgate_tune_base_learner(X, y, "binomial", base_learner = "xgboost",
n_search = 3, seed = 1)
tu$results[, c("max_depth", "eta", "val_loss", "val_perf", "best_iteration")]
}
Create a Sankey Plot (robust; aggregates to one row per subject)
Description
Visualises routing from Actual Group -> Assigned Expert (2-axis), or Actual Group -> Learned Feature Profile -> Assigned Expert (3-axis) when feature mapping is available.
Usage
plot_sankey(
prepared_data,
gnn_results,
expert_results = NULL,
top_n_per_side = 2,
use_profiles = TRUE,
verbose = FALSE
)
Arguments
prepared_data |
List from |
gnn_results |
List from |
expert_results |
Optional list from |
top_n_per_side |
Integer; number of features per side to define Profile A/B (default 2) for 3-axis. |
use_profiles |
Logical; try 3-axis when possible (default TRUE). If FALSE, always do 2-axis. |
verbose |
Logical; print progress. |
Value
A ggplot object.
Examples
if (requireNamespace("torch", quietly = TRUE) && torch::torch_is_installed()) {
data("adult_ready_small", package = "fairGATE")
d <- adult_ready_small
# drop columns with missing values or no variation before scaling
keep <- vapply(d, function(v) !anyNA(v) && (!is.numeric(v) || stats::sd(v) > 0), TRUE)
pd <- prepare_data(d[, keep], outcome_var = "income", group_var = "sex")
fit <- train_gnn(pd, run_tuning = FALSE,
best_params = list(lr = 0.01, hidden_dim = 8, dropout_rate = 0.1,
lambda = 1, temperature = 1),
num_repeats = 1, epochs = 5, seed = 1)
p <- plot_sankey(pd, fit, use_profiles = FALSE)
print(p)
}
Prepare Data for GNN Training
Description
This function takes a raw dataframe, cleans it, defines the outcome and
group variables, and scales the feature matrix. If no group_mappings
are provided, they are automatically generated from the unique values
(or factor levels) of group_var.
Usage
prepare_data(
data,
outcome_var,
group_var,
group_mappings = NULL,
cols_to_remove = NULL
)
Arguments
data |
A dataframe containing the raw data. |
outcome_var |
A string with the column name of the binary outcome (must be 0 or 1). |
group_var |
A string with the column name of the sensitive attribute. |
group_mappings |
Optional named list mapping values in |
cols_to_remove |
A character vector of column names to exclude from the feature matrix (e.g., IDs, highly collinear vars). |
Value
A list containing:
X |
The scaled feature matrix. |
y |
The numeric outcome vector. |
group |
The numeric group vector. |
feature_names |
The names of the features used. |
subject_ids |
A vector of subject IDs, if a 'subjectid' column exists. |
group_mappings |
Added as an attribute for downstream use. |
Examples
my_data <- data.frame(
subjectid = 1:10,
remission = sample(0:1, 10, replace = TRUE),
gender = sample(c("M", "F"), 10, replace = TRUE),
feature1 = rnorm(10),
feature2 = rnorm(10)
)
prepared_data <- prepare_data(
data = my_data,
outcome_var = "remission",
group_var = "gender",
cols_to_remove = c("subjectid")
)
Train and Evaluate the Gated Neural Network (robust splits + safe ROC)
Description
Trains a subgroup-aware gated neural network with a fairness-constrained loss, optionally performs hyperparameter tuning with lightweight budgets, and returns predictions, gate/expert weights, and summary metrics. Designed to be CRAN-safe:
No background installs, no saving unless requested
CPU-only torch, capped threads to avoid oversubscription
Usage
train_gnn(
prepared_data,
hyper_grid,
num_repeats = 20,
epochs = 300,
output_dir = tempdir(),
run_tuning = TRUE,
best_params = NULL,
save_outputs = FALSE,
seed = NULL,
verbose = FALSE,
tune_repeats = NULL,
tune_epochs = NULL,
splits = NULL,
family = c("binomial", "gaussian"),
n_strata = 2L,
standardise = TRUE,
impute = c("none", "median"),
penalty = c("random_effects", "bias_corrected", "variance", "thresholded"),
lambda_max = NULL,
alpha = 0,
early_stopping = FALSE,
es_prop = 0.2,
es_min_epochs = 1L,
es_patience = NULL,
base_score = NULL,
coupling = c("none", "offset", "expert", "residual"),
gate_bias_init = NULL,
gate = c("softmax", "sparsemax", "entmax15"),
intervention_cost = 0,
lambda_thr = 0,
thr_temp = 0.05,
thr_target = c("eo", "aod"),
thr_agg = c("max", "mean"),
reference_group = NULL,
gate_signal = NULL,
balance = 0,
crossed_groups = NULL,
adversary = 0,
adversary_hidden = 16L,
es_smooth = NULL,
interval_reps = 0L,
interval_epochs = 10L,
interval_level = 0.9
)
Arguments
prepared_data |
List from
|
hyper_grid |
data.frame with columns: |
num_repeats |
Integer (>=1). Repeated train/test splits for the final model (and for tuning if |
epochs |
Integer (>=1). Training epochs per run for the final model (and for tuning if |
output_dir |
Directory to write csv/rds if |
run_tuning |
Logical. If |
best_params |
data.frame/list with |
save_outputs |
Logical. If |
seed |
Optional integer seed to make data splits reproducible. If |
verbose |
Logical. Print progress messages. Default |
tune_repeats |
Integer (>=1). Repeats per combo during tuning only. Defaults to |
tune_epochs |
Integer (>=1). Epochs per run during tuning only. Defaults to |
splits |
Optional list of pre-computed train/test partitions, each a list
with integer elements |
family |
Either |
n_strata |
Integer (>=2). Number of outcome strata used by the
|
standardise |
Logical. If |
impute |
Either |
penalty |
Which fairness penalty to minimise: |
lambda_max |
Fairness weight of the most heavily constrained expert in
the frontier-anchored ladder. Defaults to |
alpha |
Weight on the auxiliary per-expert objective. |
early_stopping |
Logical. If |
es_prop |
Proportion of each partition's training rows held out for
early stopping, stratified jointly on outcome stratum and protected group.
Default |
es_min_epochs |
Minimum number of epochs before the best epoch may be
selected. Default |
es_patience |
Optional integer. When early stopping is enabled, stop
once this many consecutive epochs have passed without the validation
objective improving on its best value, and restore the best epoch.
|
base_score |
Optional list aligned with |
coupling |
How the base score enters the model: |
gate_bias_init |
Optional numeric vector added to the gate's output
bias at initialisation, one entry per gate output ( |
gate |
|
intervention_cost |
Weight |
lambda_thr, thr_temp, thr_target, thr_agg |
Weight,
temperature, per-group target ( |
reference_group |
Reference group code for the
thresholded penalty; |
gate_signal |
Optional list aligned with |
balance |
Weight of the routing-diversity regulariser (negative mutual information between individual and expert). Default 0. |
crossed_groups |
Optional two-column matrix of protected
attribute codes; the variance-component penalty becomes the crossed
decomposition of |
adversary, |
Weight of the gradient
reversal on an adversary predicting the protected group from the routing
weights, and its hidden width. |
es_smooth |
Window of the trailing moving average used
for early stopping; |
interval_reps, interval_epochs, interval_level |
Warm-started bootstrap replicates, epochs per replicate and level of the
per-person adjustment intervals. |
Value
A list with:
-
final_results(tibble: subjectid, true, prob, group, iteration) -
gate_weights(tibble with gate probabilities & entropy per subject/iteration) -
attribution(tibble: routing weights, correction intensitycorrection_intensity, anchored-expert predictionf_anchor, and fairness adjustmentdelta_fair, per held-out individual and partition; with a base score alsobase,base_link,adjustment(pred - base) andbase_weight(\pi_{i0},NAunlesscoupling = "expert"). For the coupled models the ladder quantitiesf_anchor,delta_fairandcorrection_intensityareNA, because the ladder is not defined there.) -
expert_weights(list of expert input-layer weight matrices per repeat) -
performance_summary(tibble with one row per metric, AUC for a binary outcome or R-squared and RMSE for a continuous one, and columnsMetric,Value(mean across partitions),SD,Lower,Upper(2.5th and 97.5th percentiles) andSplits) -
performance_by_split(tibble of each metric per partition) -
early_stopping(tibble of the epoch selected per partition, or a message when early stopping is disabled) -
aif360_data(tibble for fairness metric tooling) -
tuning_results(tibble or message when tuning skipped)
Coupling to a base learner
Write b_i for the base score of individual i on the link scale:
b_i = \mathrm{logit}(\mathrm{clip}(p_i, 10^{-6}, 1 - 10^{-6})) for a
binomial outcome, and b_i = (\hat y_i - \bar y_{tr}) / s_{tr} for a
gaussian one, where \bar y_{tr} and s_{tr} are the mean and
standard deviation of the outcome over the rows the model is fitted on,
the same constants the package uses to standardise the outcome, so that the
base score and the outcome share one scale. b_i is appended as an
extra input column, after the features have been standardised, to the gate
and to every expert, so the gate is
\pi_i = \mathrm{softmax}(z(x_i, b_i)/\tau) and the experts are
f_k(x_i, b_i), k = 1, \dots, K. The linear predictor is
\eta_i = b_i + \sum_{k=1}^{K} \pi_{ik} f_k(x_i, b_i)
for coupling = "offset", with the last layer of every expert initialised
at zero so that the untrained model is exactly the base learner, and
\eta_i = \pi_{i0} b_i + \sum_{k=1}^{K} \pi_{ik} f_k(x_i, b_i)
for coupling = "expert", where the gate has K + 1 outputs and
component 0 weights the base score, so \pi_{i0} is the share of the
decision delegated to the unconstrained base learner. The task loss, the
fairness penalty (on the response scale, \mathrm{sigmoid}(\eta_i) or
the standardised \eta_i), early stopping and the protected attribute's
role (objective only) are exactly as for the uncoupled model. When early
stopping is enabled the held-out slice keeps its own out-of-fold training
base scores.
Held-out predictions are assembled in double precision as
\hat y_i = \tilde b_i + [h(\eta_i) - h(b_i)], with h the inverse
link and \tilde b_i the supplied base score (clipped as above for a
binomial outcome). This equals h(\eta_i) algebraically, and makes
\hat y_i = \tilde b_i hold exactly, not merely to rounding, whenever
the network leaves an individual's link-scale prediction unchanged. The
attribution export then reports \tilde b_i, \hat y_i, the
adjustment \hat y_i - \tilde b_i, \pi_{i0} and the full routing
vector.
The fairness penalty
Individual i has prediction \hat y_i (a probability when
family = "binomial", the response scale when "gaussian"), protected
group g_i in 1:K and outcome stratum s_i in 1:Q. For a
binary outcome the strata are the two outcome classes; for a continuous one
they are n_strata equal-frequency bins of y computed on the training
rows. In cell (k, q) of size n_{kq} the mean prediction is
\hat m_{kq} and its sampling variance is
\hat v_{kq} = s^2_{kq} / n_{kq}, with s^2_{kq} the within-cell
sample variance. The total penalty is \sum_q L_q with
-
"variance":L_q = \mathrm{Var}_k(\hat m_{kq}) -
"bias_corrected":L_q = \max(0, \mathrm{Var}_k(\hat m_{kq}) - K_q^{-1}\sum_k \hat v_{kq}) -
"random_effects": the DerSimonian and Laird between-group variance component,L_q = \max(0, (Q_q - (K_q - 1)) / C_q)
The correction matters because E[S^2] = S^2(\mu) + K^{-1}\sum_k v_k:
the sample variance of the cell means carries a floor set by how precisely
those means are estimated, which no genuine improvement in fairness can
remove. The rungs nest exactly, "random_effects" reducing to
"bias_corrected" when the cell sampling variances are equal and both
reducing to "variance" as they go to zero, so the earlier method is a
special case rather than a different one. The sampling variances are always
detached from the autograd graph, so the optimiser cannot lower the penalty
by inflating within-cell spread.
Frontier-anchored experts
With \hat y_i = \sum_k \pi_{ik} f_{ik} and
\pi_i = \mathrm{softmax}(z_i/\tau), the gradient at gate logit
l is \tau^{-1}\pi_{il}(f_{il} - \hat y_i), which vanishes when
the experts agree. Identical experts are therefore a fixed point: the gate
never specialises and the experts never differentiate. Each expert is
instead given its own fairness weight on the ladder of
fairgate_lambda_ladder(), \lambda_1 = 0 and
\lambda_k = \lambda_{\max}10^{-(K-k)/(K-1)}, and the objective becomes
L = L_{task}(\hat y) + \lambda L_{fair}(\hat y) + \alpha K^{-1}
\sum_k [L_{task}(f_k) + \lambda_k L_{fair}(f_k)].
The auxiliary terms reuse the expert outputs already formed in the forward
pass, so they cost almost nothing. With \alpha = 0 the objective is
exactly the earlier one.
Per-individual attribution
For each held-out individual the fitted object reports the routing weights
\pi_i, the correction intensity
c_i = \sum_k \pi_{ik}\lambda_k / \lambda_{\max} in [0, 1], the
prediction of the accuracy-anchored expert f_{i1}, and the fairness
adjustment \delta_i = f_{i1} - \hat y_i, the amount by which the
fairness objective moved that individual. The intensity is interpretable
only when the ladder is active, that is when alpha > 0.
Examples
if (requireNamespace("torch", quietly = TRUE) && torch::torch_is_installed()) {
data("adult_ready_small", package = "fairGATE")
d <- adult_ready_small
# drop columns with missing values or no variation before scaling
keep <- vapply(d, function(v) !anyNA(v) && (!is.numeric(v) || stats::sd(v) > 0), TRUE)
pd <- prepare_data(d[, keep], outcome_var = "income", group_var = "sex")
fit <- train_gnn(pd, run_tuning = FALSE,
best_params = list(lr = 0.01, hidden_dim = 8, dropout_rate = 0.1,
lambda = 1, temperature = 1),
num_repeats = 1, epochs = 5, seed = 1)
fit$performance_summary
# Correcting an independently fitted base learner (here a logistic
# regression, cross-fitted by hand) with the residual coupling
X <- pd$X; y <- pd$y
sp <- list(list(train = 1:240, test = 241:300))
folds <- rep_len(1:5, 240)
oof <- numeric(240)
for (k in 1:5) {
m <- stats::glm.fit(cbind(1, X[sp[[1]]$train[folds != k], 1:5]),
y[sp[[1]]$train[folds != k]], family = stats::binomial())
oof[folds == k] <- stats::plogis(cbind(1, X[sp[[1]]$train[folds == k], 1:5]) %*% m$coefficients)
}
m <- stats::glm.fit(cbind(1, X[1:240, 1:5]), y[1:240], family = stats::binomial())
te <- as.numeric(stats::plogis(cbind(1, X[241:300, 1:5]) %*% m$coefficients))
fit2 <- train_gnn(pd, run_tuning = FALSE,
best_params = list(lr = 0.01, hidden_dim = 8, dropout_rate = 0.1,
lambda = 1, temperature = 1),
splits = sp, base_score = list(list(train = oof, test = te)),
coupling = "residual", epochs = 5, seed = 1)
head(fit2$attribution[, c("base", "pred", "adjustment", "base_weight")])
}