Splitknockoff

library(SplitKnockoff)

Vignette for Splitknockoff

Author : Haoxue Wang, Yang Cao, Xinwei Sun, Yuan Yao

Introduction

Split Knockoff is a data adaptive variable selection framework for controlling the (directional) false discovery rate (FDR) in structural sparsity, where variable selection on linear transformation of parameters is of concern. This proposed scheme relaxes the linear subspace constraint to its neighborhood, often known as variable splitting in optimization, which leads to better incoherence and possible performance improvement in both power and FDR. This vignette illustrates the usage of Splitknockoff with simulation experiments and will help you apply the split knockoff method in a light way. On top of that, an application to Alzheimer’s Disease study with MRI data is given as an example to illustrate that the split knockoff method can disclose important lesion regions in brains associated with the disease and connections between neighboring regions of high contrast variations during disease progression.

install.packages("SplitKnockoff")   # just one line code to install our package

This is a R implement on the Matlab version of Split Knockoffs. This R package is more convenient as glmnet can be used directly by

install.packages("glmnet'")  # just one line code to install the glmnet tool

Please update your glmnet package to the latest version for smooth usage of this package, the examples illustrated here are tested with glmnet 4.2.

For more information, please see the manual inside this package.

Key function

sk.filter(X, D, y, option) : the main function, Split Knockoff filter, for variable selection in structural sparsity problem.

function involved frequently

sk.create(X, y, D, nu, option): generate the split knockoff copy for structural sparsity problem.

W_sign(X, D, y, nu, option): generate the W statistics for split knockoff on a split LASSO path.

sk.select(W, q): this function is for variable selection based on W statistics.

For reproduction of the simulation, you can see the code as the following.

simulation details

Install all the packages and library them.
install.packages("SplitKnockoff")   # install our package


library(latex2exp)
library(ggplot2)
library(Matrix)
library(glmnet)
library(MASS)
library(SplitKnockoff)
set the parameter for the simulation
k <- 20   # sparsity level
A <- 1    # magnitude
n <- 350  # sample size
p <- 100  # dimension of variables
c <- 0.5  # feature correlation
sigma <-1 # noise level

option <- array(data = NA, dim = length(data), dimnames = NULL)

# the target (directional) FDR control
option$q <- 0.2 

# choice on threshold, the other choice is 'knockoff+'          
option$method <- 'knockoff' 

# degree of separation between original design and its split knockoff copy 
# in the range of [0, 2], the less the more separated
option$eta <- 0.1           

# whether to normalize the dataset
option$normalize <- 'true'

# choice on the set of regularization parameters for split LASSO path
option$lambda <- 10.^seq(0, -6, by=-0.01)

# choice of nu for split knockoffs
option$nu <- 10

# choice on whether to estimate the directional effect, 'disabled'/'enabled'
option$sign <- 'enabled'
generate D and X
D <- diag(p)
m <- nrow(D)

# generate X
Sigma = matrix(0, p, p)
for( i in 1: p){
  for(j in 1: p){
    Sigma[i, j] <- c^(abs(i - j))
  }
}
package mvtnorm needed for this generation, please install it in advance

library(mvtnorm) # package mvtnorm needed for this generation
set.seed(100)
X <- rmvnorm(n,matrix(0, p, 1), Sigma) # generate X
generate beta and gamma
beta_true <- matrix(0, p, 1)
for( i in 1: k){
  beta_true[i, 1] = A
  if ( i%%3 == 1){
  beta_true[i, 1] = -A
  }
}
gamma_true <- D %*% beta_true

S0 <- which(gamma_true!=0)
generate noise and y
# set random seed
set.seed(1)

# generate noise and y
varepsilon <- rnorm(n) * sqrt(sigma)
y <- X %*% beta_true + varepsilon

key step

use the key function filter to get the feature and knockoff significance

## Split Knockoff
filter_result <- sk.filter(X, D, y, option)
Z_path <- filter_result$Z
t_Z_path <- filter_result$t_Z

plot the figure 1

a <- sub("TRUE","0",is.element(1:m, S0))
b <- sub("FALSE","1",a)
df <- data.frame(x=Z_path[[1]],y= t_Z_path[[1]], feature=b)
save.image("C:/Users/v-haoxuewang/Desktop/SplitKnockoff/simu_experiments/figure_1/result/figure_1.RData")
png(file='C:/Users/v-haoxuewang/Desktop/SplitKnockoff/simu_experiments/figure_1/plot/figure_1b.png', height=1000, width=1000)
p <- ggplot(data=df,aes(x=x, y=y, color=feature))+geom_point(size=2)
p <-p + labs(title = "Split Knockoff with Lasso Statistic")
p <-p + labs(x = TeX("Value of $Z_i$"))
p <-p + labs(y = TeX("Value of $\\tilde{Z_i}$"))
p <-p + scale_fill_discrete(breaks=c("Null feature","Non-null feature"))
p <-p + geom_abline(intercept=0,slope=1 )
p <-p + scale_colour_discrete(labels=c("Null feature", "Non-null feature"))
p <-p + scale_y_continuous(limits = c(0, Z_path[[1]]))
print(p)
dev.off()

for Figure 2a, 2b, 2d, 2e, 2g and 2h of the paper simulation

# 1 June, 2021
# revised 7 July, 2021
# author:Haoxue Wang (haoxwang@student.ethz.ch)

# This script reproduces the figures on FDR and power comparison between
# Split Knockoffs and Knockoffs.
#
# The intermediate result will be automatically saved to
# '../result/temp.RData’ with the proceeding of the calculation. The final
# result will be saved to '../result/examples.mat'. The whole calculation
# may takes hours to finish.

install.packages("SplitKnockoff")   # install our package


library(latex2exp)
library(ggplot2)
library(Matrix)
library(glmnet)
library(MASS)
library(SplitKnockoff)

k <- 20   # sparsity level
A <- 1    # magnitude
n <- 350  # sample size
p <- 100  # dimension of variables
c <- 0.5  # feature correlation

option <- array(data=NA, dim = length(data), dimnames = NULL)
option$q <- 0.2
option$eta <- 0.1
option$normalize <- 'true'
option$lambda <- 10.^seq(0, -6, by=-0.01)
option$copy <- 'true'
option$sign <- 'enabled'
option <- option[-1]

# settings for nu
expo <- seq(-1, 3, by=0.2)
option$nu <- 10.^expo
num_nu <- length(option$nu)
## calculation

# generate D1, D2, and D3
D_G = matrix(0,p-1, p)

for (i in 1:p-1){
D_G[i, i] <- 1
D_G[i, i+1] <- -1
}

D_1 <-diag(p)
D_2 <- D_G
D_3 <- rbind(diag(p), D_G)
D_s <- array(data = NA, dim = length(data), dimnames = NULL)
D_s$D_1 <- D_1
D_s$D_2 <- D_2
D_s$D_3 <- D_3
D_s <- D_s[-1]



fdr_split <- array(0,c(3, num_nu, 2))
power_split <- array(0,c(3, num_nu,2))

# fdr_knock <- matrix(0, 2, 2)
# power_knock <- matrix(0, 2, 2)

num_method <- 2
method_s <- c('knockoff', 'knockoff+')


# attention! for quick speed, please set the meth manually 1 and 2
# when meth =2, set the ratio calculation offset=1 in function select
  meth = 1
  for (i in 1: 3){
# choose the respective D for each example
  sprintf('Running example for D_%d', i)
  D <- D_s[[i]]
  simu_data <- simu_unit(n, p, D, A, c, k, option)
  fdr_split[i, , meth ] <- simu_data$fdr_split
  power_split[i, , meth] <- simu_data$power_split
  # if (i < 3){
  # fdr_knock[meth, i] <- simu_data$fdr_knock
  # power_knock[meth, i] <- simu_data$power_knock
  #   }
  }
  save.image(file = "C:/Users/v-haoxuewang/Desktop/SplitKnockoff/simu_experiments/examples/result/temp.RData")


# save results
save.image("C:/Users/v-haoxuewang/Desktop/SplitKnockoff/simu_experiments/examples/result/examples.RData")
fdr_split_result <- fdr_split
power_split_result <- power_split

## for D_1
## plot for FDR
x <- expo
fdr_split <- fdr_split_result[1, ,1]
fdr_split <- array(fdr_split, dim=c(num_nu, 1))
fdr_split_plus <- fdr_split_result[1,,2]
fdr_split_plus <- array(fdr_split_plus, dim=c(num_nu, 1))


fdr <- data.frame(x,fdr_split,fdr_split_plus)
fdr[is.na(fdr)] <- 0

png(file='C:/Users/v-haoxuewang/Desktop/SplitKnockoff/simu_experiments/examples/plot/figure_11.png', height=1000, width=1000)
p <- ggplot()+geom_line(data=fdr,aes(x=x, y=fdr_split, color='fdr_split'))
p <- p +geom_line(data=fdr,aes(x=x, y=fdr_split_plus, color='fdr_split_plus'))
p <-p + labs(x = TeX("$\\log_{10} (\\nu)$"))
p <-p + labs(y = TeX("FDR"))
p <-p + geom_abline(intercept=0.2,slope=0,linetype="dashed" )
p <-p + scale_fill_discrete(breaks=c("split Knockoff", "split Knockoff+"))
p <-p + scale_colour_discrete(labels=c("split Knockoff", "split Knockoff+"))
p <-p + scale_y_continuous(limits = c(0,1))
print(p)
dev.off()




## plot for Power
x <- expo
power_split <- power_split_result[1,,1]
power_split <- array(power_split, dim=c(num_nu, 1))
power_split_plus <- power_split_result[1,,2]
power_split_plus <- array(power_split_plus, dim=c(num_nu, 1))


power <- data.frame(x, power_split,power_split_plus)
png(file='C:/Users/v-haoxuewang/Desktop/SplitKnockoff/simu_experiments/examples/plot/figure_12.png', height=1000, width=1000)
q <- ggplot()+geom_line(data=power,aes(x=x, y=power_split, color='split Knockoff'))
q <- q + geom_line(data=power,aes(x=x, y=power_split_plus, color='split Knockoff+'))
q <-q + labs(x = TeX("$\\log_{10} (\\nu)$"))
q <-q + labs(y = TeX("Power"))
q <-q + scale_fill_discrete(breaks=c("split Knockoff", "split Knockoff+"))
q <-q + scale_colour_discrete(labels=c("split Knockoff", "split Knockoff+"))
q <-q+ scale_y_continuous(limits = c(0,1))
print(q)
dev.off()


## for D_2
## plot for FDR
x <- expo
fdr_split <- fdr_split_result[2,,1]
fdr_split <- array(fdr_split, dim=c(num_nu, 1))
fdr_split_plus <- fdr_split_result[2, ,2]
fdr_split_plus <- array(fdr_split_plus, dim=c(num_nu, 1))


fdr <- data.frame(x,fdr_split,fdr_split_plus)
fdr[is.na(fdr)] <- 0
# save results

png(file='C:/Users/v-haoxuewang/Desktop/SplitKnockoff/simu_experiments/examples/plot/figure_21.png', height=1000, width=1000)
p <- ggplot()+geom_line(data=fdr,aes(x=x, y=fdr_split, color='fdr_split'))
p <- p +geom_line(data=fdr,aes(x=x, y=fdr_split_plus, color='fdr_split_plus'))
p <-p + labs(x = TeX("$\\log_{10} (\\nu)$"))
p <-p + labs(y = TeX("FDR"))
p <-p + geom_abline(intercept=0.2,slope=0,linetype="dashed" )
p <-p + scale_fill_discrete(breaks=c("split Knockoff", "split Knockoff+"))
p <-p + scale_colour_discrete(labels=c("split Knockoff", "split Knockoff+"))
p <-p + scale_y_continuous(limits = c(0,1))
print(p)
dev.off()



## plot for Power
x <- expo
power_split <- power_split_result[2,,1 ]
power_split <- array(power_split, dim=c(num_nu, 1))
power_split_plus <- power_split_result[2,,2]
power_split_plus <- array(power_split_plus, dim=c(num_nu, 1))
# power_knock_ <- power_knock[1]
# power_knock_plus_ <- power_knock[2]
# power_knock_ <- rep(power_knock_, num_nu)
# power_knock_plus_ <- rep(power_knock_plus_, num_nu)

power <- data.frame(x,power_split,power_split_plus)
png(file='C:/Users/v-haoxuewang/Desktop/SplitKnockoff/simu_experiments/examples/plot/figure_22.png', height=1000, width=1000)
q <- ggplot()+geom_line(data=power,aes(x=x, y=power_split, color='power_split'))
q <- q + geom_line(data=power,aes(x=x, y=power_split_plus, color='power_split_plus'))
q <-q + labs(x = TeX("$\\log_{10} (\\nu)$"))
q <-q + labs(y = TeX("Power"))
q <-q + scale_fill_discrete(breaks=c("split Knockoff", "split Knockoff+"))
q <-q+ scale_colour_discrete(labels=c("split Knockoff", "split Knockoff+"))
q <-q+ scale_y_continuous(limits = c(0,1))
print(q)
dev.off()



## for D_3
## plot for FDR
x <- expo
fdr_split <- fdr_split_result[3,,1 ]
fdr_split <- array(fdr_split, dim=c(num_nu, 1))
fdr_split_plus <- fdr_split_result[3,,2]
fdr_split_plus <- array(fdr_split_plus, dim=c(num_nu, 1))


fdr <- data.frame(x,fdr_split,fdr_split_plus)
fdr[is.na(fdr)] <- 0
# save results
png(file='C:/Users/v-haoxuewang/Desktop/SplitKnockoff/simu_experiments/examples/plot/figure_31.png', height=1000, width=1000)
p <- ggplot()+geom_line(data=fdr,aes(x=x, y=fdr_split, color='fdr_split'))
p <- p +geom_line(data=fdr,aes(x=x, y=fdr_split_plus, color='fdr_split_plus'))
p <-p + labs(x = TeX("$\\log_{10} (\\nu)$"))
p <-p + labs(y = TeX("FDR"))
p <-p + geom_abline(intercept=0.2,slope=0,linetype="dashed" )
p <-p + scale_fill_discrete(breaks=c("split Knockoff", "split Knockoff+"))
p <-p + scale_colour_discrete(labels=c("split Knockoff", "split Knockoff+"))
p <-p + scale_y_continuous(limits = c(0,1))
print(p)
dev.off()



## plot for Power
x <- expo
power_split <- power_split_result[3,,1 ]
power_split <- array(power_split, dim=c(num_nu, 1))
power_split_plus <- power_split_result[3,,2]
power_split_plus <- array(power_split_plus, dim=c(num_nu, 1))

power <- data.frame(x,power_split,power_split_plus)
png(file='C:/Users/v-haoxuewang/Desktop/SplitKnockoff/simu_experiments/examples/plot/figure_32.png', height=1000, width=1000)
q <- ggplot()+geom_line(data=power,aes(x=x, y=power_split, color='power_split'))
q <- q + geom_line(data=power,aes(x=x, y=power_split_plus, color='power_split_plus'))
q <-q + labs(x = TeX("$\\log_{10} (\\nu)$"))
q <-q + labs(y = TeX("Power"))
q <-q + scale_fill_discrete(breaks=c("split Knockoff", "split Knockoff+"))
q <-q+ scale_colour_discrete(labels=c("split Knockoff ", "split Knockoff+"))
q <-q+ scale_y_continuous(limits = c(0,1))
print(q)
dev.off()

save.image(file = "C:/Users/v-haoxuewang/Desktop/SplitKnockoff/simu_experiments/examples/result/final.RData")

Alzheimer’s Disease application

Disclose important lesion regions in brains associated with the disease

library(latex2exp)
library(ggplot2)
library(Matrix)
library(glmnet)
library(MASS)
library(SplitKnockoff)

option <- array(data=NA, dim = length(data), dimnames = NULL)
option$q <- 0.2
option$eta <- 0.1
option$normalize <- 'true'
option$lambda <- 10.^seq(0, -6, by=-0.01)
option$copy <- 'true'
option$sign <- 'enabled'
option <- option[-1]

# settings for nu
expo <- seq(-1, 1, by=0.1)
option$nu <- 10.^expo
num_nu <- length(option$nu)

root = getwd()
setwd(sprintf('%s/data/', root))
X = read.csv('X.csv')
X =as.matrix(X)
y = read.csv('y.csv')
y = as.matrix(y)
D = read.csv('D.csv')
D = as.matrix(D)
D = diag(90)

filter_result <- splitknockoff.filter(X, D, y, option)
results <- filter_result$results
r <- filter_result$r
save(results, file="results/region.RData")
save(r, file="results/sign.RData")

Connections between neighboring regions of high contrast variations during disease progression.

library(latex2exp)
library(ggplot2)
library(Matrix)
library(glmnet)
library(MASS)
library(SplitKnockoff)

option <- array(data=NA, dim = length(data), dimnames = NULL)
option$q <- 0.2
option$eta <- 0.1
option$normalize <- 'true'
option$lambda <- 10.^seq(0, -6, by=-0.01)
option$copy <- 'true'
option$sign <- 'enabled'
option <- option[-1]

# settings for nu
expo <- seq(-1, 1, by=0.1)
option$nu <- 10.^expo
num_nu <- length(option$nu)


# original .mat data 
root = getwd()
setwd(sprintf('%s/data/', root))
X = readMat('X.mat')
X = X$X
y = readMat('y.mat')
y = y$y
D = readMat('D.mat')
D = D$D
#
# root = getwd()
# setwd(sprintf('%s/data/', root))
# X = read.csv('X.csv')
# X =as.matrix(X)
# y = read.csv('y.csv')
# y = as.matrix(y)
# D = read.csv('D.csv')
# D = as.matrix(D)

filter_result <- splitknockoff.filter(X, D, y, option)
results <- filter_result$results
r <- filter_result$r
save(results, file="results/connection.RData")
save(r, file="results/sign.RData")

For your consideration

It usually takes 50min-80min to simulate each experiment. If you want to see the result of the experiment directly, you can reload our generated result by

load("...../SplitKnockoff/simu_experiments/figure_1/result/figure_1.RData")
load("...../SplitKnockoff/simu_experiments/examples/result/final.RData")

If you have any question or idea, please contact the author of this R package Haoxue Wang ().