井出草平の研究ノート

LOOCVを用いたLassoにおける変数選択の安定性[R]

通常のクロスバリデーションでは乱数によって結果がかなり違うため、そこで今回は一つ抜き交差検証(Leave-One-Out Cross-Validation, LOOCV)の安定性のシミュレーションを行うことにした。

LOOCVの計算の方法はこちら。

ides.hatenablog.com

また、結果にどのくらいの安定性があるか、計算のたびに異なる乱数を与え、LOOCVのLassoの計算を100回反復した。1) 独立変数側で選ばれた変数の回数、2)選ばれた時の推定値の平均とその標準偏差を計算した。

安定性のシミュレーション

# 必要なライブラリの読み込み
library(haven)
library(tidyr)
library(nestedcv)
library(doParallel)

# データの用意
auto <- haven::read_dta("http://www.stata-press.com/data/r9/auto.dta")
auto <- auto %>% drop_na()  # 全カラムに対してNAがない行を抽出

n <- nrow(auto)
price <- auto$price
X <- auto[, c("mpg", "rep78", "headroom", "trunk", "weight", "length",
              "turn", "displacement", "gear_ratio", "foreign")]
X$foreign <- as.integer(X$foreign)
X <- as.matrix(X)

# 乱数シードの設定
set.seed(123)
repeat_times <- 100  # 実際の実行時には100回に設定
seeds <- sample(1:10000, repeat_times)  # 指定された回数の乱数シードを生成

# 結果を保存するリストを初期化
coefficients_list <- list()

# 並列処理
num_cores <- detectCores() - 2

# 推定を実行
for (i in 1:repeat_times) {
  set.seed(seeds[i])
  
  fit2 <- nestcv.glmnet(
    y = price,
    x = X,
    family = "gaussian",
    alphaSet = 1,
    outer_method = "LOOCV",  # LOOCVを指定
    n_outer_folds = nrow(X),  # データポイント数に一致させる
    n_inner_folds = 10,  # 内側のCVのfold数
    cv.cores = num_cores  # 並列処理のためのコア数
  )
  
  coefficients_list[[i]] <- fit2$final_coef
}

# 変数のリスト(全ての可能な変数名)
fixed_row_names <- c("(Intercept)", "foreign", "headroom", "rep78", 
                     "displacement", "weight", "turn", "gear_ratio", 
                     "length", "trunk", "mpg")

# 各リスト要素を処理し、欠けている行をNAで埋める関数
process_coefficients <- function(coef_list, fixed_row_names) {
  df <- as.data.frame(coef_list)
  df_fixed <- data.frame(matrix(NA, nrow = length(fixed_row_names), ncol = ncol(df)))
  rownames(df_fixed) <- fixed_row_names
  colnames(df_fixed) <- colnames(df)
  df_fixed[rownames(df), ] <- df
  return(df_fixed)
}

# coefficients_listを処理
processed_list <- lapply(coefficients_list, process_coefficients, fixed_row_names = fixed_row_names)

# リストの各要素を結合して1つのデータフレームにする
final_df <- do.call(cbind, lapply(processed_list, function(x) x["coef"]))

# 変数が選ばれた回数の計算
selected_counts <- apply(final_df, 1, function(row) sum(!is.na(row)))

# 選ばれた時の平均値とその標準偏差の計算
coef_stats <- t(apply(final_df, 1, function(row) {
  valid_values <- row[!is.na(row)]
  mean_value <- mean(valid_values)
  sd_value <- sd(valid_values)
  c(mean = mean_value, sd = sd_value)
}))

# Jaccard係数の計算関数
jaccard_index <- function(set1, set2) {
  intersect_len <- length(intersect(set1, set2))
  union_len <- length(union(set1, set2))
  if (union_len == 0) return(NA)
  return(intersect_len / union_len)
}

# Jaccard係数の計算
jaccard_indices <- combn(1:repeat_times, 2, function(index) {
  set1 <- rownames(processed_list[[index[1]]])[!is.na(processed_list[[index[1]]]$coef)]
  set2 <- rownames(processed_list[[index[2]]])[!is.na(processed_list[[index[2]]]$coef)]
  jaccard_index(set1, set2)
})

mean_jaccard_index <- mean(jaccard_indices, na.rm = TRUE)

# 結果をデータフレームにまとめる
results_df <- data.frame(
  Variable = rownames(final_df),
  SelectedCount = selected_counts,
  Mean = coef_stats[, "mean"],
  SD = coef_stats[, "sd"]
)

# Jaccard係数の結果を表示
print(paste("Mean Jaccard Index:", mean_jaccard_index))

# 選ばれた回数と統計量の結果を表示
print(results_df)
                 Variable SelectedCount        Mean           SD
(Intercept)   (Intercept)           100  850.690282 3144.3525824
foreign           foreign           100 2981.745823  181.9827829
headroom         headroom           100 -462.979344   72.1114951
rep78               rep78           100  132.656012   20.0136089
displacement displacement           100   13.215741    0.6015858
weight             weight           100    2.379150    0.7957266
turn                 turn            80  -61.469561   29.2796136
gear_ratio     gear_ratio            62 -140.013670  110.0054683
length             length            32  -39.067285   17.9979634
trunk               trunk            26   31.002440   17.1044275
mpg                   mpg            24   -9.062924    5.2083239

Mean Jaccard Index: 0.798967598058507