自己紹介

  • 個人事業としてデータ分析を行う(2014年~)
  • 資格: 情報処理安全確保支援士(登録番号第025010号)
  • 趣味
    • 数学: 1年に1冊数学書を丁寧に読む
    • 読書: データサイエンス、セキュリティ、ビジネス書など
    • 料理: いろいろなレシピに挑戦(現在、137種類のレシピ)
    • DIY: メンテナンス中心(第二種電気工事士の免許あり)
  • Web: TriFields

Kanazawa.R歴

あるデータ分析者の物語

第1話:依頼

健康食品メーカーに、あるデータ分析者がいます。 新開発のサプリメントが「集中力スコア(目的変数 \(y\))」に与える影響を調査しています。 被験者から得られたデータは、次のようなデータです。

データの概要

  • 目的変数 (\(y\)): 集中力テストのスコア(標準正規分布に従う)
  • 説明変数 (\(x_1 \dots x_{20}\)): 被験者の生体バイオマーカーや生活習慣データ20項目(すべて標準化済み)

上司からの厳命

「この20項目から、スコアに寄与している真の要因を特定し、P値を添えて報告せよ。」

第2話:データの確認

データ分析者はRを立ち上げ、データを読み込みました。 一通りの前処理を行い、問題なく回帰分析ができると判断しました。

head(x, 3)
           x_01        x_02       x_03       x_04       x_05       x_06
[1,] -0.9098271  0.01888062 -0.4866893  0.2466711 -1.0968691 -1.9675214
[2,]  0.5398123 -0.49926414  1.1134220 -0.5964633  0.5494837  0.5962102
[3,]  1.3278758 -0.39958829  0.4115640  1.3547539 -0.7515418 -0.6028192
            x_07       x_08       x_09      x_10       x_11       x_12
[1,] -1.10565017  0.8108071 -0.7007359  1.762653  0.4111392  2.7714875
[2,]  0.33076430 -1.3305075 -1.1219492  1.310422  1.4267757 -1.0393164
[3,] -0.09742171  2.1322916 -0.2477852 -1.620036 -0.2836174 -0.1718471
           x_13       x_14      x_15        x_16       x_17        x_18
[1,] -1.6285182  0.3298701 1.9931675 -0.33545763 -2.9024329  0.02390451
[2,]  0.2153765 -0.9052205 0.9494623  0.56868426 -0.3880317 -1.04600325
[3,] -0.3674179  0.7768579 0.7769317  0.01666475  0.5253111 -0.81979461
          x_19      x_20
[1,] -0.617484 0.5137150
[2,]  1.381023 0.2299969
[3,]  0.665738 1.2317031
head(y, 3)
[1] -2.006276 -1.559569  1.962702

前処理の内容

  • 欠損値・外れ値の確認
  • ヒストグラムによる分布の確認
  • 変数間の相関(多重共線性)のチェック

第3話:変数選択(1)

とはいえ、説明変数が20個もあるので説明変数を絞り込むことを考えました。 一般に説明変数を絞り込むことには次のようなメリットがあります。

1. 多重共線性の回避

相関の高い説明変数を同時にモデルに投入すると、推定された係数が不安定になったり、意味の解釈が困難になったりします。不要な変数を削ることで、この問題を回避できます。

2. モデルの予測精度・汎化性能の向上

関係のない説明変数を多く含みすぎると、モデルが学習データに過剰に適合してしまい、新しいデータに対する予測精度が落ちてしまいます。変数を絞ることで、モデルをシンプルにし、予測性能を高めます。

3. 変数の重要度と解釈の明確化

変数が多すぎると、どの変数が目的変数に本当に影響を与えているのかが分かりにくくなります。不要なノイズとなる変数を取り除くことで、得られた結果の解釈が容易になります。

第3話:変数選択(2)

20個の説明変数から「効いている」変数を絞り込むため、Lasso回帰を使用しました。

# 必要なパッケージの読み込み
library(glmnet)

# 乱数の固定
set.seed(1234)

# Lasso回帰を使って「効きそうな説明変数」を自動選抜
cv_fit <- cv.glmnet(x, y, alpha = 1, standardize = FALSE)
lasso_fit <- glmnet(x, y, alpha = 1, standardize = FALSE, lambda = cv_fit$lambda.min)

# Lasso回帰の結果、係数が0でない列名を抽出
lasso_coefs <- as.matrix(coef(lasso_fit))
active_vars <- which(lasso_coefs[-1, 1] != 0)
print(paste("選ばれた説明変数:", names(active_vars)))
[1] "選ばれた説明変数: x_08" "選ばれた説明変数: x_09"

Lassoを選択した理由

  1. 多重共線性の回避
  2. モデルの予測精度向上
  3. 解釈の明確化(スパース性)

第4話:選ばれた説明変数を用いて回帰分析

いよいよ、選ばれた説明変数を用いて回帰分析を行います。

# 選択された説明変数で回帰分析
selected_x <- x[, active_vars, drop = FALSE]
model <- lm(y ~ selected_x)
summary(model)

Call:
lm(formula = y ~ selected_x)

Residuals:
     Min       1Q   Median       3Q      Max 
-2.01668 -0.47194 -0.06114  0.50683  2.56398 

Coefficients:
               Estimate Std. Error t value Pr(>|t|)  
(Intercept)     0.01351    0.10254   0.132   0.8955  
selected_xx_08  0.21658    0.10388   2.085   0.0409 *
selected_xx_09  0.25503    0.10388   2.455   0.0167 *
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 0.8579 on 67 degrees of freedom
Multiple R-squared:  0.1476,    Adjusted R-squared:  0.1222 
F-statistic: 5.801 on 2 and 67 DF,  p-value: 0.004747

第5話:解釈

これらの結果から、データ分析者は次のような解釈を行いました。

係数

  • selected_xx_08: p値が 0.0409(約4.1%)であり、5%(0.05)を下回っているため、統計的に有意な影響がありそう。
  • selected_xx_09: p値が 0.0167(約1.7%)であり、5%(0.05)を下回っているため、統計的に有意な影響がありそう。

モデルの当てはまりの良さ

  • 決定係数: 目的変数のバラツキの約14.8%を説明できている。
  • 自由度調整済み決定係数: 約12.2%となり、説明力は低い。

モデル全体の有意性

  • p値が 5%(0.05)を下回っているため、この回帰モデル全体として意味がありそう。

第6話:報告

分析者は自信を持って上司へ報告しました。

上司へ報告

「20項目のうち、x_08x_09 が有意に寄与していることが分かりました(p < 0.05)。この成分を強化した新製品の開発を提案します!」

めでたし、めでたし!?

ネタバレ

実は・・・

今回のお話において、データは次のように作っており、目的変数 \(y\) は、説明変数 \(x\) とは一切関係のないランダムなノイズです。

set.seed(1234)                                     # 乱数固定
n <- 70                                            # 説明変数の行数
p <- 20                                            # 説明変数の列数

x <- matrix(rnorm(n * p, mean = 0, sd = 1), n, p)  # 正規乱数
x <- scale(x,TRUE,TRUE)                            # 標準化
colnames(x) <- sprintf("x_%02d", 1:p)              # 説明変数の列名
y <- rnorm(n, mean = 0, sd = 1)                    # yはxと完全に無関係なノイズ

正規乱数でこのように作られたデータでは、本来、回帰分析による回帰係数が有意差を持つことはおかしいはず。

なぜ、有意差を持ってしまったのか

それは20個の説明変数の中から、「たまたま差が出た説明変数」だけを抜き出しているからです。 これを選択バイアスと呼びます。

変数選択後の「嘘」のP値

変数選択をしてからP値を計算すると、本来一様であるはずのP値が 0付近に集中 してしまいます。

set.seed(1234)
sim_p <- replicate(200, {
  xs <- matrix(rnorm(n * p), n, p)
  ys <- rnorm(n)
  cvs <- cv.glmnet(xs, ys, alpha = 1)
  fs <- glmnet(xs, ys, alpha = 1, lambda = cvs$lambda.min)
  act <- which(as.matrix(coef(fs))[-1, 1] != 0)
  if(length(act) > 0) {
    m_sim <- lm(ys ~ xs[, act, drop=FALSE])
    summary(m_sim)$coefficients[-1, 4]
  } else {
    NULL
  }
})
hist(unlist(sim_p), breaks = 20, col = "#e63946", 
     main = "変数選択後に回帰分析を行った場合のP値の分布",
     xlab = "P値", ylab = "頻度")

変数選択をせずに回帰分析を行った場合のP値

set.seed(1234)
sim_p <- replicate(200, {
  xs <- matrix(rnorm(n * p), n, p)
  ys <- rnorm(n)
  m_sim <- lm(ys ~ xs)
  summary(m_sim)$coefficients[-1, 4]
})
hist(unlist(sim_p), breaks = 20, col = "#e63946", 
     main = "変数選択をせずに回帰分析を行った場合のP値の分布",
     xlab = "P値", ylab = "頻度")

なぜP値が壊れるのか

通常の検定は、統計量が「全範囲」動けることを前提にしています。

しかし変数選択後は、「統計量が一定の閾値を超えた(選ばれた)」という領域に限定されています。

統計量の分布の変化

この制限(条件)が付くと、統計量の分布は通常の正規分布から切断正規分布 に変化します。

P値が「一様」にならない理由

P値が「正しい」とは、帰無仮説のもとで 0から1まで一様に分布することを指します。

帰無仮説が正しい(=差がない)とき、P値は「単なる偶然による偏り」の度合いを0〜1で表したものです。
「5%以下の珍しいことが起こる確率は5%」「10%以下の確率は10%」というように、どの範囲の値も等しい確率で出現するため、その分布は平ら(一様)になります。

  • 通常: \(P(Z > z) = \alpha\) となる確率がどこでも均等。
  • 選択後: すでに「大きい」ことが分かっているため、分布の右側に偏り、小さいP値が出やすくなります。

解決編:データ分割

データ分割による対処(1)

一つの対処法としては、変数選択を行うデータと回帰分析を行うデータとに分けて、それぞれを実行する。

# 必要なパッケージの読み込み
library(glmnet)

# 乱数の固定
set.seed(1234)

# データの分割
x1 <- x[1:35,]
y1 <- y[1:35]

# Lasso回帰を使って「効きそうな説明変数」を自動選抜
cv_fit <- cv.glmnet(x1, y1, alpha = 1, standardize = FALSE)
lasso_fit <- glmnet(x1, y1, alpha = 1, standardize = FALSE, lambda = cv_fit$lambda.min)

# Lasso回帰の結果、係数が0でない列名を抽出
lasso_coefs <- as.matrix(coef(lasso_fit))
active_vars <- which(lasso_coefs[-1, 1] != 0)
print(paste("選ばれた説明変数:", names(active_vars)))
[1] "選ばれた説明変数: x_01" "選ばれた説明変数: x_08" "選ばれた説明変数: x_09"

データ分割による対処(2)

# データの分割
x2 <- x[36:70,]
y2 <- y[36:70]

# 選択された説明変数で回帰分析
selected_x <- x2[, active_vars, drop = FALSE]
model <- lm(y2 ~ selected_x)
summary(model)

Call:
lm(formula = y2 ~ selected_x)

Residuals:
     Min       1Q   Median       3Q      Max 
-1.63466 -0.52183 -0.05927  0.43407  2.38828 

Coefficients:
               Estimate Std. Error t value Pr(>|t|)
(Intercept)    -0.02293    0.13267  -0.173    0.864
selected_xx_01 -0.16777    0.12541  -1.338    0.191
selected_xx_08  0.02749    0.15837   0.174    0.863
selected_xx_09  0.19947    0.13878   1.437    0.161

Residual standard error: 0.7654 on 31 degrees of freedom
Multiple R-squared:  0.1041,    Adjusted R-squared:  0.01744 
F-statistic: 1.201 on 3 and 31 DF,  p-value: 0.3256

データ分割による対処のメリット・デメリット

メリット

  • 実装が極めて簡単。
  • どんな複雑な選択ルールでも使える。

デメリット

  • サンプルサイズが減る。
  • 本来見つけられたはずの効果を見逃す可能性がある。

データを無駄にせず、正しく検定する方法はないのか?

!!!選択的推論!!!

解決編:選択的推論

選択的推論とは

選択的推論とは、「手元にあるたくさんのデータの中から、特に良さそうなものだけを選び出して分析や検証を行う際に、結果のバイアス(偏り)を正しく計算する統計的なアプローチ」のことです。

直感的イメージ

「変数として選ばれた」という事実(条件)を考慮に入れて、P値の計算をやり直します。

ハードルを上げる

「たまたま選ばれただけかもしれない」という疑い(バイアス)を数理的に評価し、検定の合格ライン(ハードル)を適切に引き上げるイメージです。

selectiveInferenceパッケージ

selectiveInference1パッケージは、前方ステップワイズ回帰、最小角度回帰、ラッソ回帰、および多平均値問題で使用できます。ラッソ関数は、ガウス分布、ロジスティック回帰、およびコックス回帰モデルを実装しています。

selectiveInferenceパッケージで選択的推論(1)

それでは、先ほどの「あるデータ分析者の物語」と同じシチュエーションで選択的推論を行ってみます。

次は、先ほどの「あるデータ分析者の物語」と同じコードです。

# 必要なパッケージの読み込み
library(glmnet)

# 乱数の固定
set.seed(1234)

# Lasso回帰を使って「効きそうな説明変数」を自動選抜
cv_fit <- cv.glmnet(x, y, alpha = 1, standardize = FALSE)
lasso_fit <- glmnet(x, y, alpha = 1, standardize = FALSE, lambda = cv_fit$lambda.min)

# Lasso回帰の結果、係数が0でない列名を抽出
lasso_coefs <- as.matrix(coef(lasso_fit))
active_vars <- which(lasso_coefs[-1, 1] != 0)
print(paste("選ばれた説明変数:", names(active_vars)))
[1] "選ばれた説明変数: x_08" "選ばれた説明変数: x_09"

selectiveInferenceパッケージで選択的推論(2)

次が実際にselectiveInferenceパッケージで選択的推論を行うコードになります。

# 必要なパッケージの読み込み
library(selectiveInference)

# 「Lassoで選んだ」という事実を条件に加えてP値を再計算
beta <- coef(lasso_fit, s = cv_fit$lambda.min / n)[-1]
output <- fixedLassoInf(x, y, 
                        beta = beta, 
                        lambda = cv_fit$lambda.min * n)

# 補正された結果を表示(P値が正しく大きな値に戻る)
print(output)

Call:
fixedLassoInf(x = x, y = y, beta = beta, lambda = cv_fit$lambda.min * 
    n)

Standard deviation of noise (specified or estimated) sigma = 0.886

Testing results at lambda = 14.521, with alpha = 0.100

 Var  Coef Z-score P-value LowConfPt UpConfPt LowTailArea UpTailArea
   8 0.217   2.019   0.570    -1.091    0.338        0.05      0.049
   9 0.255   2.378   0.228    -0.288    0.434        0.05      0.049

Note: coefficients shown are partial regression coefficients

fixedLassoInf関数のlambda引数について(1)

glmnet1パッケージのglmnet関数を用いたLasso回帰は、次のような最適化問題を解きます。

\[ \hat{\beta} = \arg \min_{\beta} \left\{ \frac{1}{2n} \| \mathbf{y} - \mathbf{X}\beta \|_2^2 + \lambda_{g} \| \beta \|_1 \right\} \]

selectiveInference2パッケージのfixedLassoInf関数で想定されているLasso回帰の最適化問題は次です。

\[ \hat{\beta} = \arg \min_{\beta} \left\{ \frac{1}{2} \| \mathbf{y} - \mathbf{X}\beta \|_2^2 + \lambda_{s} \| \beta \|_1 \right\} \]

fixedLassoInf関数のlambda引数について(2)

この2つの式を比較すると、最適化される\(\beta\)を同じにするためには、ペナルティ項の\(\lambda\)に以下の関係が成り立つ必要があります。

重要

\[\lambda_s = n \times \lambda_g\]

glmnetselectiveInference では数式の定義が異なるため、次のように n 倍する必要があります。

# 「Lassoで選んだ」という事実を条件に加えてP値を再計算
beta <- coef(lasso_fit, s = cv_fit$lambda.min / n)[-1]
output <- fixedLassoInf(x, y, 
                        beta = beta, 
                        lambda = cv_fit$lambda.min * n)

選択的推論のメリット・デメリット

メリット

  • 全データを使用できる
  • Lassoなどの明確なアルゴリズムに対し、数学的に厳密。

デメリット

  • 計算コスト: パス全体を追うため、大規模データでは時間がかかる。
  • アルゴリズム依存: 選択ルールが数式化されている必要がある。

比較実験:データ分割 vs 選択的推論

データ分割 (Data Splitting)

  • サンプルを半分に分ける(\(N/2\))。
  • モデルの学習と検定を完全に分離。
  • 安全だが、検定力が低い。

選択的推論 (Selective Inference)

  • 全サンプルを使う(\(N\))。
  • 数理モデルで「選択の代償」を支払う。
  • 数学的に高度だが、検定力が高い。
# 簡易シミュレーション比較
set.seed(1234)
n_sim <- 100
n_samples <- 60
p_vars <- 10
signal <- 0.4

power_res <- replicate(n_sim, {
  # データ生成
  X <- matrix(rnorm(n_samples * p_vars), n_samples, p_vars)
  # 第1変数だけ真の効果がある
  Y <- signal * X[, 1] + rnorm(n_samples)

  # 1. 選択的推論 (全データ)
  fit_all <- glmnet(X, Y, alpha = 1, lambda = 0.1)
  # 簡易的な評価(実際はもっと複雑)
  out_si <- tryCatch(fixedLassoInf(X, Y, beta = as.vector(coef(fit_all, s=0.1)[-1]), lambda = 0.1*n_samples), error=function(e) NULL)
  p_si <- if(!is.null(out_si) && out_si$vars[1] == 1) out_si$pv[1] else 1
  
  # 2. データ分割 (半分)
  idx <- 1:(n_samples/2)
  fit_split <- glmnet(X[idx,], Y[idx], alpha = 1, lambda = 0.1)
  selected <- which(as.vector(coef(fit_split, s=0.1)[-1]) != 0)
  p_split <- 1
  if(1 %in% selected) {
    m_test <- lm(Y[-idx] ~ X[-idx, 1])
    p_split <- summary(m_test)$coefficients[2, 4]
  }

  c(SI = p_si < 0.05, Split = p_split < 0.05)
})

power_df <- data.frame(
  Method = c("選択的推論", "データ分割"),
  Power = rowMeans(power_res, na.rm = TRUE)
)

ggplot(power_df, aes(x = Method, y = Power, fill = Method)) +
  geom_bar(stat = "identity") +
  theme_minimal(base_size = 16) +
  scale_fill_manual(values = c("#a8dadc", "#457b9d")) +
  labs(title = "真の効果を検出できる確率 (Power)", y = "検出力") +
  ylim(0, 1)

まとめ

より良いデータ分析のためのチェックリスト

  1. 「まずはLassoで絞って、有意差を見よう」と思いませんでしたか?
    • その瞬間、通常のP値は使えなくなっています。
  2. サンプルサイズは十分ですか?
    • 余裕があるなら「データ分割」が最も頑健で簡単です。少ないなら「選択的推論」を検討しましょう。
  3. 選択ルールは明確ですか?
    • 「なんとなく良さそう」という直感による選択は、補正が最も困難です。可能な限り、アルゴリズムに基づいた選択を行いましょう。

ここだけは!ポイント

  1. 選択バイアスはどこにでも潜んでいる
    • データを眺めてから「この変数が良さそうだ」と思った時点で、あなたのP値は壊れ始めています。
  2. 「嘘の発見」に騙されない
    • 変数選択後の単純な回帰分析は、過剰に「有意」を出しやすい。
  3. 選択的推論という武器を持つ
    • サンプルサイズが貴重な現場こそ、数学的な補正が威力を発揮します。

参考文献

Jerome H. Friedman, Rob Tibshirani, Trevor Hastie. 2010. 「Regularization Paths for Generalized Linear Models via Coordinate Descent | Journal of Statistical Software」. https://www.jstatsoft.org/article/view/v033i01.
Ryan Tibshirani, Jonathan Taylor, Rob Tibshirani. 2019a. 「Help for package selectiveInference」. https://cran.r-project.org/web/packages/selectiveInference/refman/selectiveInference.html.
———. 2019b. 「CRAN: Package selectiveInference」. https://cran.r-project.org/web/packages/selectiveInference/index.html.