どの変数が重要なのかを「説明」する道具としての回帰分析

回帰分析には「予測」のほかに、他の変数の影響を差し引いて見たい効果を取り出す「調整」、そしてどの変数が重要なのかを知る「説明」という使い方があります。前回の記事では調整を扱ったので、今回は説明の話です。例えば顧客満足度のアンケートを回帰にかけて、どの項目が総合評価のドライバーになっているのかを探るようなケースが挙げられます(key driver 分析とも呼ばれます)。

知りたいのは各項目の「重要度」ですが、回帰分析の出力のどの数字をそれとして読めばよいのかは、案外はっきりしません。満足度アンケートのダミーデータを用いてそのあたりを整理していきたいと思います。

満足度アンケートのデータ

あるサブスク型アプリで、ユーザー2,000人にアンケートを取ったとします。質問は4つで、機能への満足、サポートへの満足、価格への満足(各1〜5点)と、「このアプリを友人や同僚に勧めたいと思いますか」という推奨度(0〜10点)です。実際のアンケートなら質問の設計はもっと丁寧に考えるところですが、ここでは説明用に単純化しています。知りたい問いは、推奨度を左右しているのはどの項目か、です。

データはシミュレーションで作ります。機能とサポートの満足は強めに相関させ(0.7)、価格への満足は他の2項目と弱めの相関(0.3)にします。推奨度との相関は機能0.52、サポート0.42、価格0.43としました。相関行列を np.linalg.cholesky に通すと、指定した相関を持つ乱数が作れます。

import numpy as np
import pandas as pd

rng = np.random.default_rng(42)
n = 2000

# 変数間の相関だけを決める: 機能・サポート・価格・推奨度 の順
R = np.array([
    [1.00, 0.70, 0.30, 0.52],  # 機能
    [0.70, 1.00, 0.30, 0.42],  # サポート
    [0.30, 0.30, 1.00, 0.43],  # 価格
    [0.52, 0.42, 0.43, 1.00],  # 推奨度
])
z = rng.standard_normal((n, 4)) @ np.linalg.cholesky(R).T

# アンケートの形に落とす: 分位点で切って整数スケールにする
def to_scale(x, lo, hi):
    edges = np.quantile(x, np.linspace(0, 1, hi - lo + 2)[1:-1])
    return np.digitize(x, edges) + lo

df = pd.DataFrame({
    "func": to_scale(z[:, 0], 1, 5),
    "support": to_scale(z[:, 1], 1, 5),
    "price": to_scale(z[:, 2], 1, 5),
    "reco": to_scale(z[:, 3], 0, 10),
})

点数の刻みに丸めた分、相関は指定からすこし目減りします。できたデータで確かめておきます。

df[["func", "support", "price"]].corr().round(2)
#          func  support  price
# func     1.00     0.64   0.26
# support  0.64     1.00   0.25
# price    0.26     0.25   1.00

各項目と推奨度との相関(単相関)はこうです。

df[["func", "support", "price"]].corrwith(df["reco"]).round(2)
# func       0.47
# support    0.37
# price      0.41

係数の大小と単位

まずは単純に重回帰分析にかけて、係数の大小がどうなるか見てみましょう。

import statsmodels.formula.api as smf

model = smf.ols("reco ~ func + support + price", df).fit()
model.params.round(2)
# Intercept    0.15
# func         0.77
# support      0.16
# price        0.69

round(model.rsquared, 3)
# 0.316

係数と一緒に出した R² は決定係数と呼ばれる値で、目的変数のばらつき(分散)のうち、説明変数の側で説明できている割合を表します。1なら完全に説明でき、0ならまったく説明できていません。今回は0.316なので、推奨度のばらつきの約3割がこの3項目で説明でき、残りの7割は3項目では説明のつかない要因や個人差、という読み方になります。

係数の読み方は、「他の項目の点数を一定としたうえで、その項目が1点上がったときに、推奨度が何点変わるか」です。機能が0.77点、価格が0.69点、そしてサポートは0.16点となりますので、単相関では価格とほぼ同格だったサポートが、ここでは4分の1以下になっていることがわかります(この差にはあとで触れます)。

この結果について、係数の大小をそのまま重要度と読んでよいでしょうか。これについては、読むことはできない場合が多いです。係数は「1単位あたり」の値なので、単位の取り方しだいで数字が変わります。試しに、価格の満足度だけ100点満点に換算してみます。

df["price100"] = df["price"] * 20
model = smf.ols("reco ~ func + support + price100", df).fit()
model.params.round(3)
# func         0.772
# support      0.159
# price100     0.034

中身は同じ情報なのに、係数は 0.69 → 0.034 と約20分の1になりました。1点の意味が変わったからです。今回は3項目とも1〜5点で揃っているのでまだ比べる余地がありそうに見えますが、利用歴(月)や月間ログイン回数のような単位の違う変数が説明変数に混ざった途端、係数の大小比較は最初から成立しなくなります。

標準化係数でも残る問題

単位の問題への標準的な対処は、標準化です。各変数から平均を引いて標準偏差で割り、すべての変数を「平均0、ばらつき1」のスケールに揃えてから回帰します。係数は「その変数が1標準偏差動いたときに、目的変数が何標準偏差動くか」になり、単位が消えます。

cols = ["reco", "func", "support", "price"]
zdf = (df[cols] - df[cols].mean()) / df[cols].std()
model = smf.ols("reco ~ func + support + price", zdf).fit()
model.params.round(2)
# func         0.35
# support      0.07
# price        0.31

これで得られるパラメータの推定値は標準化係数と呼ばれる値です。

単位の問題はこれで消えましたがそれでも、サポートの位置づけは変わっていません。単相関では0.37対0.41とほぼ同格だったサポートと価格が、標準化係数では0.07対0.31と4倍を超える差です。この数字を根拠に「サポートは推奨度にほとんど効いていない」と報告してよいものでしょうか。

こうしたことが生じる原因は、機能とサポートの相関0.64にあります。回帰係数は「他の変数を一定としたときの」傾きでした。機能の点数を固定すると、サポートの点数のうち機能と連動して動く部分も一緒に固定されます。サポートと推奨度の結びつき0.37のかなりの部分は機能と共有された結びつきで、係数はその共有部分をサポートの取り分に数えません。

ベン図にするとこうなります。円はそれぞれの変数のばらつき(分散)を表し、円の重なりの面積が相関の強さ(相関係数の2乗)に対応します(価格は話を単純にするため省いています)。

機能とサポートがそれぞれ推奨度と重なり、両者が共有する部分cが真ん中にできる

  • a: 機能だけが推奨度と重なっている部分
  • b: サポートだけが推奨度と重なっている部分
  • c: 機能とサポートの両方が推奨度と重なっている部分

単相関は、機能に a+c を、サポートに b+c を数えます。cが2回数えられているので、各項目の単相関の2乗を足し上げると、回帰全体の説明力(R²)より大きくなります。逆に標準化係数に対応するのは a と b、つまり他の変数と重ならない独自部分で、cはどの変数の取り分にもなりません。(※係数は面積そのものではなく、この独自部分だけを使って測った傾きです。)サポートの係数が小さいのは独自部分bが小さいためであって、cまで含めたサポートと推奨度の重なりそのものは、実際には価格に大きく劣るわけではありません。

つまり「どの変数が重要か」という同じ問いに対して、単相関では共有部分を関係する変数全員に配る答えを、係数は共有部分をどの変数にも配らない答えを返しています。ですので、共有部分cをどう配るかを決めない限り重要度は1つに定まりません。

なお、面積の足し引きで押し切れるのはこの図が成り立つ素直な場合に限られ、変数の組み合わせによっては成り立たないこともあります。この記事では素直な場合だけを考えます。

相対的重み付け分析(RWA)

重なりがあるせいで配れないのなら、重なりのない説明変数に置き換えてから配ればよい。これが相対的重み付け分析(Relative Weight Analysis; Johnson, 2000)の発想です。

手順は2段階です。まず、元の3項目に「それぞれできるだけ近い」かつ「互いには無相関」という都合のよい変数のセットを数学的に作り、それで推奨度を回帰します。無相関なら重なりの領域がないので、R²は各変数の取り分にきれいに分かれます。次に、その取り分を「元の項目とどれだけ近いか」に応じて元の3項目へ割り戻します。

この割り戻しが必要なのは、作った変数が元の項目そのものではないからです。たとえば「機能に近い変数」は、機能とサポートが相関している都合で機能そのものにはなれず、今回のデータでは中身のおよそ9割が機能、1割がサポートになっています。そこでこの変数の取り分も、9:1で機能とサポートへ配ります。3つの変数の取り分を同じように中身の割合で配り、項目ごとに合計したものが最終的な重要度です。

相関した元の項目では重なりcの配り先が決まらないが、無相関な変数に置き換えるとR²がきれいに分かれ、その取り分を近さに応じて元の項目へ割り戻す

Python では relativeImp というパッケージで手軽に計算できます。

from relativeImp import relativeImp

relativeImp(df, outcomeName="reco",
            driverNames=["func", "support", "price"]).round(3)
#     driver  rawRelaImpt  normRelaImpt
# 0     func        0.135        42.721
# 1  support        0.059        18.524
# 2    price        0.122        38.754

rawRelaImpt が各項目の重要度、normRelaImpt はそのシェア(%)です。重要度の合計は0.316で、さきほどの重回帰のR²にちょうど一致します。RWAは、回帰が説明したR²を各項目に配り分ける方法だ、という関係です。シェアで見ると機能43%、価格39%、サポート19%。標準化係数ではほぼゼロに見えたサポートが、重要度としては2割弱を分担しています。

標準化係数とRWAのどちらかが正しいのではなく、答えている問いが違います。標準化係数は「他の項目を固定したまま、サポートだけが1標準偏差上がったら推奨度はどうなるか」に、RWAは「推奨度のばらつきのうち、サポートの動きと結びついている分はどれだけか」に答えています。ドライバーの候補を見渡したいという冒頭の目的に沿うのは、後者の問いのほうでしょう。

ドミナンス分析

共有部分の配り方は、RWAの一通りではありません。もう一つよく知られているのがドミナンス分析(Budescu, 1993)です。発想はこちらのほうが直接的で、変数を式へ入れる順番をすべて試します。

説明変数を1つずつ式に追加していくと、追加のたびにR²が増えますが、増分は入れる順番に依存します。サポートを最初に入れれば、機能と共有している部分cの重なりもサポートの増分に入りますし、機能のあとに入れれば、cはすでに機能に取られたあとです。そこで、すべての順番(3変数なら6通り)でR²の増分を計算し、その平均を各変数の重要度とします。順番によってcの行き先が入れ替わるので、全順番で平均を取ることが、そのまま共有部分の配り分けになっています。

from itertools import permutations

drivers = ["func", "support", "price"]

def r2(cols):
    return smf.ols("reco ~ " + " + ".join(cols), df).fit().rsquared if cols else 0.0

contrib = {c: [] for c in drivers}
for order in permutations(drivers):
    for i, c in enumerate(order):
        contrib[c].append(r2(list(order[: i + 1])) - r2(list(order[:i])))

pd.Series({c: np.mean(v) for c, v in contrib.items()}).round(3)
# func       0.136
# support    0.060
# price      0.120

合計はやはりR²の0.316に一致し、シェアに直すと機能43%、価格38%、サポート19%。RWAとほぼ同じ配分です。出発点の発想が違う2つの方法ですが、実データでは近い値を返すことが多いと知られていて、今回もそのとおりになりました。

両分析では計算量が大きく異なります。ドミナンス分析は変数の組み合わせごとにR²を計算する必要があるため、変数が増えると急に重くなります(20項目のアンケートなら、組み合わせは100万通りを超えます)。RWAは発想の異なる別の方法ですが、ほぼ同じ配分を行列の変換ひとつで得られるため、項目の多いアンケートでも軽く回ります。

数字の使い分けとまとめ

ここまで、サポートの数字は単相関0.37、回帰係数0.16、標準化係数0.07、RWAのシェア19%と揺れてきました。どれも同じデータから出た正しい数字で、それぞれ意味するものが違います。整理するとこうなります。

数字意味するもの共有部分の扱い
単相関その変数だけで見た、目的変数との連動の強さ各変数に重複して反映される
回帰係数他の変数を固定したときの、その変数1単位あたりの目的変数の変化反映されない
標準化係数他の変数を固定したときの、その変数1標準偏差あたりの目的変数の変化反映されない
RWA・ドミナンス分析回帰が説明したばらつき(R²)のうち、その変数に配り分けられる取り分各変数に配分される

ベン図の言葉でいえば、単相関は b+c を、回帰係数と標準化係数は b だけを、RWA・ドミナンス分析は b に c の分け前を足したものをサポートの取り分として測っています。

最後に、この重要度という数字の効力の範囲を確かめておきます。サポートの重要度19%は、「サポートを改善すれば推奨度の19%分が上がる」という意味ではありません。ここまで計算してきたのはすべて、観察されたアンケートの中で、どの項目が推奨度と連動して動いているかという相関の記述です。サポートの満足度を実際に動かしたときに推奨度がどう動くかは、前回の記事で扱った調整の問題で、交絡を考えずには答えられません。また、このシェアは回帰式に入れた項目セットの中における配分なので、回帰式に投入する項目を増減すれば各項目のシェアも動く点にも留意が必要です。

効果の推定ではなく、どの項目が推奨度と強く結びついているかを知ること自体が目的なら、要因分析はここで完結してかまいません。その際にどの数字を答えとするかは見たいものしだいで、RWAやドミナンス分析の配分はその候補の1つです。共有部分まで含めて重要度を測りたい場面に合っていて、係数が小さいという理由だけで「サポートは重要でない」と結論しかける読み違いも防げます。そのうえで「ではサポートを改善すれば推奨度は上がるのか」まで踏み込みたいときに、実験や調整つきの分析の出番が来ます。

参考

  • Johnson, J. W. (2000). A heuristic method for estimating the relative weight of predictor variables in multiple regression. Multivariate Behavioral Research, 35(1), 1–19.
  • Budescu, D. V. (1993). Dominance analysis: A new approach to the problem of relative importance of predictors in multiple regression. Psychological Bulletin, 114(3), 542–551.