Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

因子分析

因子分析(factor analysis)はデータの分散を分解することによって何らかの共通性を抽出する手法である。

心理学などの分野において、「リーダシップ」や「社交性」のような直接観測することができない構成概念(construct)を抽出するために用いられる。

ビジネスにおいてはどういったところで応用されるのか?

心理学以外の利用例

因子分析モデル

1因子モデル

pp次元の観測値がnn個得られているとし、ii番目のjj次元のデータをxijx_{ij}と表すことにする。xijx_{ij}を標準化して平均0、分散1にしたデータをzijz_{ij}と表すことにする。

1因子モデルは標準化データを次のように分解するモデルである。

独自因子・独自係数は誤差項eeとしてまとめる表記

zij=ajfi+eijz_{ij} = a_j f_i + e_{ij}

も存在する。独自因子と独自係数はどちらも自由に推定しようとすると解が定まらない(djd_jを2倍にしてuiju_{ij}を半分にしてもいい)ので、推定の際は通常はdj=1d_j=1に固定する。

1因子モデルをベクトルで表すと次のようになる

因子スコアfif_{i}が観測対象(アンケート回答者など)ごとに算出されるため、個々の観測対象のスコアリングができる。

mm因子モデル

mm個の因子を扱うように拡張した場合は次のように表される。

ベクトルの場合は

zj=aj1f1+aj2f2++ajmfm+ej=(f1f2fm)(aj1aj2ajm)+ej=Faj+ej\begin{aligned} \mathbf{z}_j &= a_{j1} \mathbf{f}_1 + a_{j2} \mathbf{f}_2 + \cdots + a_{jm} \mathbf{f}_m + \mathbf{e}_j \\ &= \begin{pmatrix} \mathbf{f}_1 & \mathbf{f}_2 & \cdots & \mathbf{f}_m \end{pmatrix} \begin{pmatrix} a_{j1} \\ a_{j2} \\ \vdots \\ a_{jm} \end{pmatrix} + \mathbf{e}_j \\ &= \mathbf{F} \mathbf{a}_j + \mathbf{e}_j \end{aligned}

と表すことができる。

因子分析モデル(行列表現)

観測値ごとの添字をつけるとi×j×li \times j \times lで3方向の次元(テンソル)になってしまうので簡略化のため観測値ではなく確率変数として扱うと、よりシンプルなモデルになる。

設問項目(item)がII個あり、II個の確率変数x1,,xIx_1,\dots,x_Iが得られたとする。それを平均0分散1へと標準化したものをz1,,zIz_1,\dots,z_Iとする。これらがTT個の因子(trait)から影響を受けていると仮定し、TT個の共通因子f1,,fTf_1,\dots,f_Tと因子負荷量λ11,λ12,,λIT\lambda_{11},\lambda_{12},\dots,\lambda_{IT}

zi=λi1f1++λiTfT+εi(i=1,,I)z_i = \lambda_{i1} f_1 + \cdots + \lambda_{iT} f_T + \varepsilon_i \quad (i = 1,\dots, I)

と説明する線形モデルが因子分析モデルとなる。行列とベクトルで表すと次のようになる

また、共通因子f\mathbf{f}と独自因子ε\mathbf{\varepsilon}には次の仮定がおかれる:

  • f\mathbf{f}ε\mathbf{\varepsilon}は統計的に独立

  • E[f]=0,Var[f]=Φ\operatorname{E}[\mathbf{f}] = 0, \quad \operatorname{Var}[\mathbf{f}] = \mathbf{\Phi}

  • E[ε]=0,Var[ε]=Ψ\operatorname{E}[\boldsymbol{\varepsilon}] = 0, \quad \operatorname{Var}[\boldsymbol{\varepsilon}] = \mathbf{\Psi}

    • Ψ\mathbf{\Psi}は対角行列、つまり 異なる変数に対する独自因子は無相関とする)

この仮定により、観測変数の分散共分散行列を考えると

Var[z]=Σ=ΛΦΛ+Ψ\operatorname{Var}[\mathbf{z}] =\mathbf{\Sigma} =\mathbf{\Lambda \Phi \Lambda^\top} + \mathbf{\Psi}

となる。つまり、 観測された変数の分散共分散行列をモデルのパラメータで説明するモデル であることがわかる。

z\mathbf{z}を中心化ではなく標準化している場合、分散共分散行列ではなく相関係数行列に対して上記の構造化を考えることになる。

(参考:因子分析 - Wikipedia

パラメータの推定

因子分析モデルのパラメータは観測変数の相関行列をもとにして最尤推定法などで推定される。

Λ,Ψ\mathbf{\Lambda}, \mathbf{\Psi}の推定(最尤推定法による例)

標本共分散行列S\mathbf{S}を使って、尤度関数は

L(Λ,Ψ)=n2(logΣ+tr(SΣ1))\mathcal{L}(\mathbf{\Lambda}, \mathbf{\Psi})=-\frac{n}{2}(\log |\mathbf{\Sigma}|+\operatorname{tr}(\mathbf{S} \mathbf{\Sigma}^{-1}))

と表される。これをEMアルゴリズムなどで最大化してΛ,Ψ\mathbf{\Lambda}, \mathbf{\Psi}の推定値を得る。

f\mathbf{f}の推定(バートレット法による例)

最小二乗法にちょっと似てる数式で、独自因子の分散の逆行列で重み付け(誤差が大きい場合はスコアへの寄与が小さくなる)を行いつつ推定する

f^=(ΛΨ1Λ)1ΛΨ1x\hat{\mathbf{f}}=\left(\boldsymbol{\Lambda}^{\top} \mathbf{\Psi}^{-1} \boldsymbol{\Lambda}\right)^{-1} \boldsymbol{\Lambda}^{\top} \mathbf{\Psi}^{-1} \mathbf{x}

実装例

import pandas as pd

# テストの点数データ
scores = pd.DataFrame([
    {"英語": 98, "国語": 95, "数学": 75},
    {"英語": 90, "国語": 75, "数学": 70},
    {"英語": 60, "国語": 57, "数学": 80},
    {"英語": 50, "国語": 70, "数学": 60},
    {"英語": 30, "国語": 50, "数学": 40},
])
Z = (scores - scores.mean()) / scores.std()

from statsmodels.multivariate.factor import Factor

fa = Factor(Z, n_factor=1, method="ml").fit()
fa.summary()
/usr/local/lib/python3.10/site-packages/statsmodels/multivariate/factor.py:417: UserWarning: Fitting did not converge
  warnings.warn("Fitting did not converge")
Loading...
fa.uniqueness
array([0.00228581, 0.22772564, 0.46534024])
# 共通因子スコア
fa.factor_scoring(Z)
array([[ 1.15171807], [ 0.85998399], [-0.19932969], [-0.54814633], [-1.26422604]])

主成分分析との違い

主成分分析も分散を分解して共通した成分を取り出す手法であり、因子分析と似ているが、誤差を分離するかどうかが異なる。

因子分析は観測変数を共通した部分と誤差とに分解した上で共通因子を抽出するのに対し、主成分分析は誤差の分離を行わずに共通した部分を取り出そうとする。

妥当性と信頼性

信頼性係数

真の得点の分散の、テスト得点の分散に対する比を、テスト得点yy信頼性係数 (reliability coefficient) という。

ρ=σt2σy2=σt2σt2+σe2=1σe2σy2\rho=\frac{\sigma_t^2}{\sigma_y^2} =\frac{\sigma_t^2}{\sigma_t^2+\sigma_e^2}=1-\frac{\sigma_e^2}{\sigma_y^2}

信頼性係数は測定の誤差分散が小さいほど1に近い値をとり、誤差分散が大きいとき0に近い値をとる。

クロンバックのα\alpha係数

信頼性係数ρ\rhoは母集団レベルの話であり、標本の情報から推定するには別の値を使う。 信頼性係数の推定方法はいくつかあるが、「項目を超えた一貫性」に関する検討を行うクロンバックのα係数が代表的。

「項目を超えた一貫性」とは「テストに含まれる項目群を、同様の別の項目群に置き換えても結果は安定しているか」という考え方で、そういう意味での信頼性を近似的に与える指標がクロンバックのα係数である。

クロンバックのα\alpha
α=JJ1(1j=1Jsj2sy2)\alpha=\frac{J}{J-1}\left(1-\frac{\sum_{j=1}^J s_j^2}{s_y^2}\right)
  • JJ:項目数

  • sy2s_y^2:テスト得点の分散

  • sj2s_j^2:項目jjの分散

α\alpha係数は

  • 項目間の相関が高いとき

  • 項目数が多いとき

に高い値をとる。項目が多いだけでも高くなってしまうので注意が必要で参考程度に用いられる。

測定の標準誤差(SEM)は信頼性係数の推定値ρ^\hat\rhoを用いて推定される。

測定の標準誤差(SEM)の推定量
SEM=sy1ρ^\operatorname{SEM} = s_y \sqrt{1-\hat{\rho}}

Messickの妥当性概念

Messickは妥当性検証のための側面を6つ挙げた。

側面(facet)内容(簡潔な説明)
① 内容的妥当性(Content)測定内容が理論的構成概念をどれだけ網羅しているか
② 構成概念妥当性(Substantive)測定が理論に基づき、認知的に妥当か
③ 構造的妥当性(Structural)得点構造が想定した構成概念と整合しているか(因子構造など)
④ 外的妥当性(External)他の指標との関係が理論通りか(収束的・弁別的妥当性)
⑤ 結果的妥当性(Consequential)測定の利用が社会的に望ましい結果をもたらすか(公正性・偏りなど)
⑥ 実証的妥当性(Generalizability)測定結果が一般化可能か(状況・集団を超えて妥当か)

上記6つの側面にわたる証拠が備えられていることが望ましい。しかし妥当性検証とは尺度が利用されることで明らかになる証拠を積み重ねるプロセスであるため、最初から全部を備えることは不可能。

探索的因子分析

探索的因子分析(exploratory factor analysis: EFA)は、因子と観測変数の関係についての仮説や制約を置かずに、観測変数のみから相関係数を計算し、観測変数間に相関関係をもたらす因子を推定する方法。

たとえばモデルの因子の数は固有値から探索して決める。例えば「1より大きい固有値をもつ因子を採用する」とする(Guttman基準)

サンプルデータを生成して因子分析を実行してみる

Source
import numpy as np
import pandas as pd

def generate_factor_analysis_data(n_samples=1000, n_variables=3, noise_std=0.1, random_state=42):
    """因子分析のサンプルデータを生成する"""
    np.random.seed(random_state)
    n_factors = 1 # 1因子モデル

    # 因子負荷量行列(loadings): n_variables × n_factors
    # loadings = np.random.uniform(0.5, 0.9, size=(n_variables, n_factors))
    loadings = np.array([
        [0.9],
        [0.5],
        [0.1],
    ])

    # 潜在因子スコア(factors): n_samples × n_factors
    factors = np.random.normal(size=(n_samples, n_factors))

    # ノイズ項(ユニーク性): n_samples × n_variables
    noise = np.random.normal(scale=noise_std, size=(n_samples, n_variables))

    # 観測変数: n_samples × n_variables
    data = factors @ loadings.T + noise

    # データフレームに変換
    columns = [f"X{i+1}" for i in range(n_variables)]
    df = pd.DataFrame(data, columns=columns)
    return df, factors, loadings

# サンプルデータ生成
df, true_factors, true_loadings = generate_factor_analysis_data()

display(pd.DataFrame(true_factors).head().style.set_caption("真の因子スコアの最初の5行"))
display(pd.DataFrame(true_loadings).style.set_caption("因子負荷量"))
display(df.head().style.set_caption("サンプルデータの最初の5行"))
Loading...
Loading...
Loading...

statsmodelsパッケージでの推定

from statsmodels.multivariate.factor import Factor
fa = Factor(endog=df, n_factor=1, method="ml").fit()
fa.rotate("promax")
fa.summary()
/usr/local/lib/python3.10/site-packages/statsmodels/multivariate/factor_rotation/_gpa_rotation.py:101: RuntimeWarning: divide by zero encountered in log10
  table.append([i_try, f, np.log10(s), al])
Loading...
factors_ = fa.factor_scoring(df)
display(pd.DataFrame(factors_).head().style.set_caption("因子スコアの最初の5行"))
Loading...

factor-analyzerパッケージでの推定

from factor_analyzer import FactorAnalyzer
fa = FactorAnalyzer(n_factors=1, rotation='promax', method='ml', is_corr_matrix=False)
fa.fit(df)

# 因子負荷量(loadings)
loadings = pd.DataFrame(fa.loadings_, index=df.columns, columns=["第1因子"])
loadings.style.set_caption("因子負荷量")
/usr/local/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.
  warnings.warn(
/usr/local/lib/python3.10/site-packages/factor_analyzer/factor_analyzer.py:663: UserWarning: No rotation will be performed when the number of factors equals 1.
  warnings.warn(
Loading...
# 相関行列を渡す場合
from factor_analyzer import FactorAnalyzer
fa = FactorAnalyzer(n_factors=1, rotation='promax', method='ml', is_corr_matrix=True)
fa.fit(df.corr())

# 因子負荷量(loadings)
loadings = pd.DataFrame(fa.loadings_, index=df.columns, columns=["第1因子"])
loadings.style.set_caption("因子負荷量")
/usr/local/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.
  warnings.warn(
/usr/local/lib/python3.10/site-packages/factor_analyzer/factor_analyzer.py:663: UserWarning: No rotation will be performed when the number of factors equals 1.
  warnings.warn(
Loading...

固有値を高い順に並べる スクリープロット

Source
# スクリープロットで因子数を探索
import matplotlib.pyplot as plt
import matplotlib_fontja

eigen = fa.get_eigenvalues()
left = list(range(1, len(eigen[0])+1))
fig, ax = plt.subplots()
ax.plot(left, eigen[0], marker="o")
ax.set(xlabel="因子数(固有値の数)", ylabel="固有値", title="スクリープロット")
ax.axhline(y=1, color="gray", linestyle="--", alpha=0.5)
fig.show()
<Figure size 640x480 with 1 Axes>

scikit-learnの因子分析

# 2因子モデルで因子分析を実行(scikit-learn)
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_std = scaler.fit_transform(df)

from sklearn.decomposition import FactorAnalysis
fa = FactorAnalysis(n_components=1, random_state=0, rotation="varimax")
fa.fit(X_std)

# 因子スコア
factors_ = fa.transform(X_std)

# 因子負荷量(loadings)
loadings_ = pd.DataFrame(fa.components_.T, index=df.columns, columns=["第1因子"])  # shape: (n_variables, n_factors)
loadings_.style.set_caption("因子負荷量")
Loading...
plt.scatter(true_factors, factors_)
<Figure size 640x480 with 1 Axes>

確認的因子分析

確認的因子分析(confirmatory factor analysis: CFA)は、因子の数や観測変数との関係性などについての仮説(モデル)をあらかじめ立てておき、その仮説の正しさを検証するためにモデルをデータにあてはめていく。

その妥当性はデータの分散をモデルがどれだけ説明できたか(適合度)の指標などによって確認される。

import semopy

カテゴリカル因子分析

通常の因子分析では、観測変数も連続値(正規分布を仮定)であることを前提にしている。しかし実際の調査データでは、

  • 「1(全くそう思わない)」〜「5(非常にそう思う)」のリッカート尺度

  • 「ある/ない」「はい/いいえ」の2値データ

  • 評価カテゴリ(例:低・中・高)

といった順序尺度のカテゴリカル変数が含まれることがある。

因子分析では相関行列をもとに計算できるため、カテゴリカル変数であっても相関関係へと変換してから扱えば通常の計算手続きに持ち込むことができる。

カテゴリカル変数の相関関係には「観測値はカテゴリカル変数だが、その背後には連続値の潜在変数があり、ある閾値をこえるかどうかで離散的な観測値になっている」という考え方で統計モデルを組んで相関係数を算出するタイプのものが存在する。この仮定は因子分析モデルの仮定(離散値の背後には潜在的な因子スコアが存在する)とも整合的なのでそういった相関係数が使われる。具体的には、

  • 連続変数 × カテゴリカル変数 の相関 → ポリシリアル(polyserial)相関係数

  • カテゴリカル変数 × カテゴリカル変数 の相関 → ポリコリック(polychoric)相関係数

が使われる。Pythonだと ordinalcorr パッケージで計算できる。

実装例

先程のサンプルデータを離散化してみる

Source
def discretize_to_categorical(df, n_bins=4, labels=None):
    """
    各列をカテゴリカルに変換(等頻度ビンに分割)
    
    Parameters:
        df: pd.DataFrame - 入力の連続値データ
        n_bins: int - ビンの数(カテゴリ数)
        labels: list or None - カテゴリラベル。Noneなら自動で0~(n_bins-1)

    Returns:
        pd.DataFrame - カテゴリカル変数データ
    """
    if labels is None:
        labels = list(range(n_bins))

    df_cat = df.copy()
    for col in df.columns:
        df_cat[col] = pd.qcut(df[col], q=n_bins, labels=labels)
    return df_cat

# カテゴリカルに変換
df_cat = discretize_to_categorical(df)
df_cat.head().style.set_caption("サンプルデータの最初の5行")
Loading...

相関行列に変換する

from ordinalcorr import hetcor
corr_matrix = hetcor(df_cat)
corr_matrix
Loading...

FactorAnalyzerでis_corr_matrix=Trueを指定して相関行列を入力データとして渡すようにする

from factor_analyzer import FactorAnalyzer
fa = FactorAnalyzer(n_factors=1, rotation='promax', method='ml', is_corr_matrix=True)
fa.fit(corr_matrix)

# 因子負荷量(loadings)
loadings_cat = pd.DataFrame(fa.loadings_, index=df.columns, columns=["第1因子"])
loadings_cat.style.set_caption("因子負荷量(カテゴリカル)")
/usr/local/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.
  warnings.warn(
/usr/local/lib/python3.10/site-packages/factor_analyzer/factor_analyzer.py:663: UserWarning: No rotation will be performed when the number of factors equals 1.
  warnings.warn(
Loading...
factors_cat = fa.transform(df)
plt.scatter(true_factors, factors_cat)
/usr/local/lib/python3.10/site-packages/sklearn/utils/deprecation.py:151: FutureWarning: 'force_all_finite' was renamed to 'ensure_all_finite' in 1.6 and will be removed in 1.8.
  warnings.warn(
/usr/local/lib/python3.10/site-packages/factor_analyzer/factor_analyzer.py:766: UserWarning: Could not find original mean and standard deviation; usingthe mean and standard deviation from the current data set.
  warnings.warn(
<Figure size 640x480 with 1 Axes>

カテゴリカル変数にする前の連続変数に対する推定結果とある程度近い推定値となっていることがわかる

(loadings_cat - loadings).abs().style.set_caption("連続変数の場合との推定値の絶対誤差")
Loading...
Source
# スクリープロットで因子数を探索
eigen = fa.get_eigenvalues()
left = list(range(1, len(eigen[0])+1))
fig, ax = plt.subplots()
ax.plot(left, eigen[0], marker="o")
ax.set(xlabel="因子数(固有値の数)", ylabel="固有値", title="スクリープロット")
ax.axhline(y=1, color="gray", linestyle="--", alpha=0.5)
fig.show()
<Figure size 640x480 with 1 Axes>

例2:実際のアンケートデータ

bfi データ(Big Five Inventory)は、心理学でよく使われる**5因子性格モデル(OCEAN)**に基づいたアンケートデータ

25個の性格項目(各5問 × 5因子)

  • A1 ~ A5:Agreeableness(協調性)

  • C1 ~ C5:Conscientiousness(誠実性)

  • E1 ~ E5:Extraversion(外向性)

  • N1 ~ N5:Neuroticism(神経症傾向)

  • O1 ~ O5:Openness(開放性)

import statsmodels.api as sm

# psychパッケージの Big Five Inventory(性格5因子) データ
df = sm.datasets.get_rdataset("bfi", package="psych").data
df.head()
Loading...
from statsmodels.multivariate.factor import Factor
fa = Factor(endog=df.iloc[:, 0:25], n_factor=5, method="ml").fit()
fa.rotate("varimax")
fa.get_loadings_frame()
Loading...
# シンプルにしてみる
df = df.iloc[:, 0:10].dropna()

from statsmodels.multivariate.factor import Factor
fa = Factor(endog=df, n_factor=2, method="ml").fit()
fa.rotate("varimax")
fa.get_loadings_frame()
Loading...
df.corr().style.format("{:.3f}").background_gradient()
Loading...
# 相関係数をとってみる
from ordinalcorr import hetcor
corr = hetcor(df)
corr.style.format("{:.3f}").background_gradient()
Loading...
fa = Factor(corr=corr, n_factor=2, method="ml").fit()
fa.rotate("varimax")
fa.get_loadings_frame()
Loading...
# カテゴリの数を減らしてみる
df_cat = df.copy()
for col in df.columns:
    df_cat[col] = pd.cut(df[col], bins=3, labels=range(3)).astype(int)
# 相関係数をとってみる
from ordinalcorr import hetcor
corr = hetcor(df_cat, n_unique=50)
corr.style.format("{:.3f}").background_gradient()
Loading...
fa = Factor(corr=corr, n_factor=2, method="ml").fit()
fa.rotate("varimax")
fa.get_loadings_frame()
Loading...

参考