CCI is an R package for testing conditional independence, \(Y _||_ X \mid Z\): does \(X\) carry information about \(Y\) once \(Z\) is known? It uses machine learning to answer this as a prediction question. If a model for \(Y\) predicts new data better with the real \(X\) than with a randomly permuted \(X\), then \(X\) carries information about \(Y\) beyond \(Z\). The null distribution is built with permutation and Monte Carlo cross-validation, so the test needs no parametric assumptions about the relationships between the variables.
The method is described in Thorjussen et al. (2024, Algorithms) and the package in Thorjussen et al. (2026, SoftwareX).
From CRAN:
install.packages("CCI")The development version from GitHub:
# install.packages("remotes")
remotes::install_github("khliland/CCI", build_vignettes = TRUE)We simulate data where \(Y\) and \(X\) both depend on \(Z_1\) and \(Z_2\), but not on each other. Then \(Y _||_ X \mid Z_1, Z_2\) is true, while \(Y _||_ X \mid Z_1\) is false.
library(CCI)
normal_data <- function(n) {
Z1 <- rnorm(n)
Z2 <- rnorm(n)
X <- Z1 + Z2 + rnorm(n)
Y <- Z1 + Z2 + rnorm(n)
data.frame(Z1, Z2, X, Y)
}
set.seed(123)
dat <- normal_data(500)
summary(CCI.test(Y ~ X | Z1 + Z2, data = dat, seed = 1))Computational Conditional Independence Test
--------------------------------------------
Method: CCI test using rf
Formula: Y ~ X | Z1 + Z2
Permutations: 160
Metric: RMSE
Tail: left
Statistic: 1.143
P-value: 0.7019
MC sample: 1
The p-value is large, so conditional independence is not rejected. Leaving out \(Z_2\):
summary(CCI.test(Y ~ X | Z1, data = dat, seed = 1))Computational Conditional Independence Test
--------------------------------------------
Method: CCI test using rf
Formula: Y ~ X | Z1
Permutations: 160
Metric: RMSE
Tail: left
Statistic: 1.492
P-value: 0.006211
MC sample: 1
Here the null hypothesis is rejected. The p-value is the smallest
possible with 160 Monte Carlo samples, 1/161. plot() shows
the null distribution together with the test statistic.
The formula. Y ~ X | Z1 + Z2 tests
\(Y _||_ X \mid Z_1, Z_2\).
Y ~ X + Z1 + Z2 is the same. For an unconditional test of
\(Y _||_ X\), write
Y ~ X | 1 or Y ~ X + 1.
Data types. The metric is chosen from the type of
\(Y\): RMSE for a numeric \(Y\), Cohen’s Kappa for a factor, character
or logical \(Y\).
metric = "LogLoss" uses predicted class probabilities
instead. When \(Z\) contains
categorical variables, \(X\) is
permuted within their groups (robust = TRUE).
Learners. method = "rf" (default),
"xgboost", "svm" or "KNN". Model
parameters are passed directly,
e.g. CCI.test(Y ~ X | Z, data, method = "xgboost", eta = 0.1, max_depth = 3).
Large data. MC_sample = "Auto"
(default) uses a share \((900/n)^{0.75}\) of the data in each Monte
Carlo sample when \(n > 900\), and
method = "KNN" is by far the fastest learner.
Judging the result.
res <- CCI.test(Y ~ X | Z1, data = dat, parametric = TRUE) # p-value from a normal approximation
plot(res) # the null distribution should be unimodal
QQplot(res) # p-values over new train/test splitsTuning and test direction. tune = TRUE
tunes the learner with CCI.pretuner() before testing, and
choose_direction = TRUE tests in the direction where the
prediction is easiest.
Your own model or metric.
r_squared <- function(actual, predictions) {
1 - sum((actual - predictions)^2) / sum((actual - mean(actual))^2)
}
CCI.test(Y ~ X | Z1, data = dat, metricfunc = r_squared, tail = "right") # higher R^2 is better
lm_rmse <- function(formula, data, train_indices, test_indices, ...) {
fit <- lm(formula, data = data[train_indices, ])
actual <- data[test_indices, all.vars(formula)[1]]
sqrt(mean((predict(fit, data[test_indices, ]) - actual)^2))
}
CCI.test(Y ~ X | Z1, data = dat, mlfunc = lm_rmse, tail = "left") # lower RMSE is betterThe vignettes cover the package in detail:
| Vignette | Contents |
|---|---|
| Getting started with CCI | The idea behind the test, reading the output, formulas, learners, data types and large data |
| Diagnostics, tuning and test direction | Null distribution plots, nperm and parametric p-values,
QQplot(), tuning and choose_direction, with a
checklist |
| Custom models and performance metrics | Writing metricfunc and mlfunc functions,
for regression and classification |
| Applied examples | Testing the conditional independencies implied by a causal DAG, and time series |
In R: browseVignettes("CCI"), or
e.g. vignette("Testing-CI-with-CCI", package = "CCI").
What has changed between versions is listed in NEWS.md.
If you use CCI, please cite:
citation("CCI") gives the references in BibTeX
format.
Please report bugs and suggestions at https://github.com/khliland/CCI/issues. Contributions are welcome.