Up

因子分析: 主因子法

Python

 

 

 因子分析は、因子という潜在変数の1次式によってデータ(変量)の変動を説明するモデルであり、次式(1)

 

(1)                

 

によって表わされ、多くの解法が提案されている。よく用いられるものの1つとして、主因子法がある。主因子法では、標準化された変量に対して、因子と残差項の独立性の仮定の下で、以下の繰り返しにより因子パターン(因子負荷量)が求められる。

 

 

(A)誤差項の分散を対角要素とする行列を

 

 

とおく。初期値は、である。

 

(B)相関行列を対しての固有分解

 

 

から、固有値の大きいもの個を選び

 

 

をおく。

 

C

 

 

と更新する。

の値が収束していると判定されれば、終了。収束していると判定されなければ、ステップ(B)に戻る。

 

 

詳しくは、岡本「心理学データ分析と測定」, pp.86-89を参照されたい。

 

主因子法のスクリプトをリスト1のように作成してみた。ファイルは、まとめて圧縮ファイルfapfpyfiles.zipとした。

 

入力データファイルは、Excelファイルとして図1a,bのように用意する。図1a,bのデータは、岡本(2014、表5.1.1を入力したものである。

 

図1a

図1b

 

1行目に変数名を設定する。1列目は、ケースIDの番号を設定する。この値は、分析には用いられない。2列目以降に、データ値を設定する。

 

リスト1のスクリプトを実行すると、入力データファイル名を聞いてくる。次の例では、図1のデータファイル名Data6Subj100.xlsxが設定されている。入力データファイルは、スクリプトファイルと同じフォルダ内にあるものとする。

 

Input file name(*.xlsx) = Data6Subj100.xlsx

Output file name(*.txt) = Results.txt

 

入力データファイル名を設定すると、出力ファイル名の設定が求められる。適当なテキストファイル名を設定する。上の例では、Results.txtが設定されている。出力ファイルは、スクリプトと同じフォルダ内に保存される。

出力テキストファイル名を設定すると、計算が始まり、固有値のスクリープロットが表示される(図2)。

 

図2

 

図2の場合、因子数は2が適当であると判断される。

図2のWindowを閉じると、因子数qの値の設定が「q = 」と求められる(図3)。

 

図3

 

図3では、2が設定されている。

因子数qを設定して、Enterキーを押すと、主因子法の計算が始まる。

因子数qの値によっては、ヘイウッドケースなど不適切な解が得られることがあるが、そのときは他の方法を試せばよい。直接最小2乗法による因子分析ウェブサイトも用意している。

主因子法の計算が終了すると、回転前の因子負荷量Aの値の布置が初期値として表示される(図4)。

 

図4

 

図4のWindowを閉じると、次に表示する布置の横軸(X軸)の表す因子の指定が求められる(図5)。

 

図5

 

因子として0以下の整数値を設定すると、初期値による布置の表示が終了する。

布置に表示する因子として0以下の整数値を設定して、初期値による布置の表示を終了すると、次にバリマックス回転による直交回転が行われる。

バリマックス回転は、次の基準を最小化するものとして、足立・山本(2024p.137のアルゴリズムにより行った(リスト2)。

 

 

バリマックス回転後の布置は、図6のように表示される。

 

図6

 

図6のWindowを閉じると、次の布置の表示に用いる横軸の表す因子の設定が求められる。

因子として0以下の整数値を設定すると、バリマックス回転後の布置の表示は終了する。

次に、斜交回転としてコーティミン回転が行われる。

コーティミン基準は

 

 

で与えられ、足立・山本(2024p.140のアルゴリズムを参考にしてリスト2に示すスクリプトを用意した。

斜交回転が終了すると、スクリプトの実行終了である(図7)。

 

図7

 

実行終了後、出力ファイルResults.txtを開くと、以下のようである。

 

Input fie = Data6Subj100.xlsx.

var_names =

['Jpn' 'Eng' 'Hist' 'Math' 'Phys' 'Chem']

 

1: [48 46 60 47 68 44]

2: [54 49 63 61 49 45]

3: [48 55 47 49 49 47]

98: [84 84 96 69 60 69]

99: [39 51 39 59 60 65]

100: [47 45 60 68 72 70]

 

 

                mean        sd

Jpn           58.750    15.213

Eng           59.310    15.591

Hist          59.310    15.059

Math          60.520    13.056

Phys          60.090    13.287

Chem          58.940    13.981

 

R =

                 Jpn       Eng      Hist      Math      Phys      Chem

       Jpn   1.00000   0.84066   0.87327   0.23810   0.19741   0.29807

       Eng   0.84066   1.00000   0.82001   0.20751   0.12885   0.24563

      Hist   0.87327   0.82001   1.00000   0.27644   0.20387   0.24637

      Math   0.23810   0.20751   0.27644   1.00000   0.60998   0.67641

      Phys   0.19741   0.12885   0.20387   0.60998   1.00000   0.84128

      Chem   0.29807   0.24563   0.24637   0.67641   0.84128   1.00000

 

 

Eigen values of R

                                 Cum.Sum    Cum.Prop(%)

         1        3.25276        3.25276          54.21

         2        1.86791        5.12067          85.34

         3        0.42494        5.54561          92.43

         4        0.20646        5.75207          95.87

         5        0.14753        5.89960          98.33

         6        0.10040        6.00000         100.00

 

 

Before rotation

 

                 Factor-1       Factor-2 Communalitites

Jpn              -0.82978       -0.45147        0.89236

Eng              -0.75926       -0.46560        0.79326

Hist             -0.81036       -0.44279        0.85276

Math             -0.55047        0.44045        0.49701

Phys             -0.58987        0.65074        0.77141

Chem             -0.70145        0.65671        0.92330

 

 

Varimax rotation

 

            Factor-1  Factor-2  Communalities

Jpn         -0.93379   0.14280        0.89236

Hist        -0.91308   0.13796        0.85276

Eng         -0.88621   0.08887        0.79326

Chem        -0.16094   0.94731        0.92330

Phys        -0.07573   0.87503        0.77141

Math        -0.17163   0.68378        0.49701

Contri         2.552     2.178

 

 

Quartimin rotation

 

                 Factor-1       Factor-2

Jpn              -0.93987        0.01669

Hist             -0.91928        0.01459

Eng              -0.89909       -0.03229

Chem             -0.02114        0.95479

Phys              0.05557        0.89213

Math             -0.07238        0.68142

 

 

Structure matrix

                 Factor-1       Factor-2

Jpn              -0.94451        0.27816

Hist             -0.92334        0.27034

Eng              -0.89011        0.21784

Chem             -0.28677        0.96067

Phys             -0.19263        0.87667

Math             -0.26196        0.70156

 

 

 

参考文献

足立浩平・山本倫生(2024)主成分分析と因子分析:特異値分解を出発点として.共立出版

岡本安晴(2019)いまさら聞けないPythonでデータ分析:多変量解析、ベイズ統計分析.丸善出版

岡本安晴(2014)心理学データ分析と測定:データの見方と心の測り方.勁草書房

 

 

 

リスト1 主因子法のスクリプト(pfa_main.py

 

import pandas as pd

import numpy as np

from mymodule import *     #   arrng_pttn, draw_map,

from rotate import *       #   Varimax_Orthomax, Oblique_Quartimin

 

 

fin_nm = input('Input file name(*.xlsx) = ')

df_data = pd.read_excel(fin_nm)            #   Excelファイルの読み込み

fout_nm = input('Output file name(*.txt) = ')

fout = open(fout_nm, 'w')                    #   出力用テキストファイルのオープン

fout.write(f'Input fie = {fin_nm}.\n')

var_names = np.array(df_data.keys())

caseIDs = np.array(df_data[var_names[0]])

var_names = var_names[1:]                  #   変数名リスト               

print('var_names =\n', var_names)

fout.write(f'var_names =\n{var_names}\n\n')

print('n =', len(caseIDs))

X = np.array(df_data.values)[:,1:]         #   変数値の配列(行列)

 

for i, v in zip(caseIDs, X):

    print(i, v)

    fout.write(f'{i}: {v}\n')

 

means = np.mean(X, axis=0)

sds = np.std(X, axis=0)

print()

print(f'{" ":10s}{"mean":>10s}{"sd":>10s}')

fout.write(f'\n\n{" ":10s}{"mean":>10s}{"sd":>10s}\n')

for i, v in enumerate(var_names):

    print(f'{v:<10s}{means[i]:>10.3f}{sds[i]:>10.3f}')

    fout.write(f'{v:<10s}{means[i]:>10.3f}{sds[i]:>10.3f}\n')

 

   

R = np.corrcoef(X, rowvar=False)      #   相関行列

print()

print('相関行列')

fout.write('\nR =\n')

print(f'{" ":>10s}', end='')

fout.write(f'{" ":>10s}')

for v in var_names:

    print(f'{v:>10s}', end='')

    fout.write(f'{v:>10s}')

fout.write('\n')

print()

for i, s in enumerate(var_names):

    print(f'{s:>10s}', end='')

    fout.write(f'{s:>10s}')

    for j in range(len(var_names)):

        print(f'{R[i][j]:>10.5f}', end='')

        fout.write(f'{R[i][j]:>10.5f}')

    print()

    fout.write('\n')

 

Lmbd, V = np.linalg.eigh(R)       #   相関行列の固有分解

idx = np.argsort(-Lmbd)           #   固有値の降順に並べ替えるためのインデックス

Lmbd = Lmbd[idx]

V = V.T[idx].T

 

n_vars = len(var_names)

cum_sum = np.cumsum(Lmbd)

print('\n相関行列の固有値')

fout.write('\n\nEigen values of R\n')

print(f'{"Cum.Sum":>40s}{"Cum.Prop(%)":>15s}')

fout.write(f'{"Cum.Sum":>40s}{"Cum.Prop(%)":>15s}\n')

for i in range(n_vars):

    print(f'{i+1:>10d}{Lmbd[i]:>15.5f}', end ='')

    print(f'{cum_sum[i]:>15.5f}{100*cum_sum[i]/cum_sum[n_vars-1]:>15.2f}')

    fout.write(f'{i+1:>10d}{Lmbd[i]:>15.5f}')

    fout.write(f'{cum_sum[i]:>15.5f}{100*cum_sum[i]/cum_sum[n_vars-1]:>15.2f}\n')

 

"""

       固有値のスクリープロット

"""

plt.title('Scree plot', fontsize=16)

plt.plot(np.arange(n_vars)+1, Lmbd, ls='-')

plt.ylabel('Eigen values', fontsize=14)

plt.tight_layout()

plt.show()

 

q = int(input('q = '))   #   因子数の入力

if q <= 0:

    q = 1

if q > n_vars:

    q = n_vars

 

"""

          主因子法

"""

Psi = np.zeros((n_vars))

RPsi = R - np.diag(Psi)

ckcnt = 0

while True:

    Lmbd, V = np.linalg.eigh(RPsi)

    idx = np.argsort(-Lmbd)

    Lmbd = Lmbd[idx]

    V = V.T[idx].T

    for ck in range(q):

        if Lmbd[ck] < 1.0e-9:

            q = ck

            print(q)

            break

    A = V[:,:q] * (Lmbd[:q]**0.5)

    R1 = A @ A.T

    Psi1 = np.diag(np.identity(n_vars) - R1)

    ckcnt += 1

    ck_cnv = np.sum(np.abs(Psi1 - Psi))

    if (ck_cnv < 1.0e-7) or (ckcnt > 1000):

        print('\nck_cnv =', ck_cnv, '   ckcnt =', ckcnt)

        print()

        break

    Psi = Psi1

    RPsi = R - np.diag(Psi)

 

"""

        共通性

"""

communlty = np.sum(A ** 2, axis=1)

print('\n初期解')

fout.write('\n\nBefore rotation\n')

print(f'{" ":10s}', end='')

fout.write(f'\n{" ":10s}')

for i in range(q):

    tstr = f"Factor-{i+1}"

    print(f'{tstr:>15s}', end='')

    fout.write(f'{tstr:>15s}')

print(f'{"Communalities":>15s}')

fout.write(f'{"Communalitites":>15s}\n')

for i in range(n_vars):

    print(f'{var_names[i]:<10s}', end='')

    fout.write(f'{var_names[i]:<10s}')

    for j in range(q):

        print(f'{A[i][j]:>15.5f}', end='')

        fout.write(f'{A[i][j]:>15.5f}')

    print(f'{communlty[i]:>15.5f}')

    fout.write(f'{communlty[i]:>15.5f}\n')

"""

        初期値のマップ

"""

dim1 = 1

dim2 = 2 if q >= 2 else 1

while True:

    draw_map(var_names, A.T[dim1-1], A.T[dim2-1],

             'Before Rotation',

             f'Factor-{dim1}', f'Factor-{dim2}')

    dim1 = int(input('dim_x = '))

    if dim1 <= 0:

        break

    if dim1 > q:

        dim1 = q

    dim2 = int(input('dim-y = '))

    if dim2 <= 0:

        break

    if dim2 > q:

        dim2 = q

"""

        バリマックス回転

"""

var_rot = Varimax_Orthomax(A)

A_vmax = var_rot.rotate()

print('\nバリマックス回転')

fout.write('\n\nVarimax rotation\n')

#for i in range(n_vars):

#    print(var_names[i], '  ', A_vmax[i])

 

v_names, pttn = arrng_pttn(var_names, A_vmax)

 

dim1 = 1

dim2 = 2 if q >= 2 else 1

while True:

    draw_map(var_names, A_vmax.T[dim1-1], A_vmax.T[dim2-1],

             'Varimax rotation', f'dim-{dim1}', f'dim-{dim2}')

    dim1 = int(input('dim_x = '))

    if dim1 <= 0:

        break

    if dim1 > q:

        dim1 = q

    dim2 = int(input('dim-y = '))

    if dim2 <= 0:

        break

    if dim2 > q:

        dim2 = q

 

comm_vmax = np.sum(pttn**2, axis=1)

contrib = np.sum(pttn**2, axis=0)

print('\nバリマックス回転後の因子パターン')

print(f'{" ":10s}', end='')

fout.write(f'\n{" ":10s}')

for i in range(q):

    vs = f'Factor-{i+1}'

    print(f'{vs:>10s}', end='')

    fout.write(f'{vs:>10s}')

print(f'{"Communalities":>15s}')

fout.write(f'{"Communalities":>15s}\n')

for i in range(n_vars):

    print(f'{v_names[i]:<10s}', end='')

    fout.write(f'{v_names[i]:<10s}')

    for j in range(q):

        print(f'{pttn[i][j]:>10.5f}', end='')

        fout.write(f'{pttn[i][j]:>10.5f}')

    print(f'{comm_vmax[i]:>15.5f}')

    fout.write(f'{comm_vmax[i]:>15.5f}\n')

 

print(f'{"Contri":<10s}', end='')

fout.write(f'{"Contri":<10s}')

for j in range(q):

    print(f'{contrib[j]:>10.3f}', end='')

    fout.write(f'{contrib[j]:>10.3f}')

print()

print()

fout.write('\n\n')

 

"""

       コーティミン回転

"""

q_rot = Oblique_Quartimin(A)

A_qrot, Phi = q_rot.rotate()

 

v_names, pttn = arrng_pttn(var_names, A_qrot)

print('\nコーティミン回転後の因子パターン\n')

fout.write('\nQuartimin rotation\n')

print(f'{" ":10s}', end='')

fout.write(f'\n{" ":10s}')

for i in range(q):

    vs = f'Factor-{i+1}'

    print(f'{vs:>15s}', end='')

    fout.write(f'{vs:>15s}')

print()

fout.write('\n')

for i in range(n_vars):

    print(f'{v_names[i]:<10s}', end='')

    fout.write(f'{v_names[i]:<10s}')

    for j in range(q):

        print(f'{pttn[i][j]:>15.5f}', end='')

        fout.write(f'{pttn[i][j]:>15.5f}')

    print()

    fout.write('\n')

 

"""

        構造行列

"""

Strctr = A_qrot @ Phi

v_names, pttn = arrng_pttn(var_names, Strctr)

print('\n構造行列')

fout.write('\n\nStructure matrix\n')

print(f'{" ":10s}', end='')

fout.write(f'{" ":10s}')

for i in range(q):

    vs = f'Factor-{i+1}'

    print(f'{vs:>15s}', end='')

    fout.write(f'{vs:>15s}')

print()

fout.write('\n')

for i in range(n_vars):

    print(f'{v_names[i]:<10s}', end='')

    fout.write(f'{v_names[i]:<10s}')

    for j in range(q):

        print(f'{pttn[i][j]:>15.5f}', end='')

        fout.write(f'{pttn[i][j]:>15.5f}')

    print()

    fout.write('\n')

 

fout.close()

print(f'\n{fout_nm} is saved.\n')

 

 

 

 

リスト2 回転のスクリプト(rotate.py

 

"""

       Y.Okamoto, 2025.06

 

       参考文献

       足立浩平・山本倫生(2024)

       主成分分析と因子分析、共立出版

"""

 

import numpy as np

import scipy.optimize as so

 

 

class Varimax_Orthomax:

    def __init__(self, C):

        """

                C: 負荷行列

        """

        self.C = C

 

    def rotate(self):

        """

               足立・山本(2014)、p.137

        """

        p = len(self.C)

        Mw = np.identity(p) - np.ones((p,p))/p    #  w==1

        Gmma = self.C

 

        while True:

            Gmma2 = Gmma * Gmma

            S_tilde = Gmma * (Mw @ Gmma2)

            U, Delta, Vh = np.linalg.svd(self.C.T @ S_tilde)

            T = U @ Vh

            Gmma_new = self.C @ T

            ck = np.sum((Gmma - Gmma_new)**2)

            Gmma = Gmma_new

            #print(ck)

            if ck < 1.0e-6:

                break

        return Gmma     #   回転後の行列

 

 

 

class Oblique_Quartimin():

    """

            C: 負荷行列

    """

    def __init__(self, C):

        self.C = C

 

    def rotate(self):

        """

               足立・山本(2014)、p.140

        """

        p, m = np.shape(self.C)

        Gmma = self.C

        Phi = np.identity(m)

        T = np.identity(m)

 

        def calc_tjk(gmma, phi, j, k):

 

            class Gammas():

                def __init__(self, C, Phi, j, k):

                    #self.c = Gmma

                    self.C = C

                    self.Phi = Phi

                    self.j = j

                    self.k = k

 

                def f_gammas(self, t):

                    """

                          Quartimin criterion

                    """

                    c = self.C

                    Phi = self.Phi

                    p = np.shape(c)[0]

                    j = self.j

                    k = self.k

                    v = 0.0

                    for i in range(p):

                        g2_ij = (1 - 2*t*Phi[j][k] + (t**2)) * (c[i][j]**2)

                        g2_ik = (t**2) * (c[i][j]**2) + \

                                2 * t * c[i][j] * c[i][k] + c[i][k]**2

                        v += g2_ij * g2_ik

                    return v

 

            calc_g = Gammas(gmma, phi, j, k)

            result = so.minimize_scalar(calc_g.f_gammas, [-10, 10])

            #print(result)

            return result.x                   

 

        n_try = 0

        while True:

            Gmma0 = Gmma

            for j in range(m):

                for k in range(m):

                    if j != k:      #   j < k j != k に変更

                        tjk = calc_tjk(Gmma, Phi, j, k)

                        tjj = (1 - 2 * tjk * Phi[j][k] + (tjk**2)) ** 0.5

                        T_jk = np.identity(m)

                        T_jk[j][j] = tjj

                        T_jk[j][k] = tjk

                        Gmma = Gmma @ T_jk

                        inv_T_jk = np.linalg.inv(T_jk)

                        Phi = inv_T_jk @ Phi @ (inv_T_jk.T)

                        T = T @ T_jk

            n_try += 1

 

            ck = np.sum((Gmma0 - Gmma) ** 2)

            #print('ck =', ck, '   n_try =', n_try)

            if (ck < 1.0e-6) or (n_try > 50):

                break

            else:

                T = np.identity(m)

 

        return Gmma, Phi      #   回転後の負荷行列と共分散行列

 

 

 

 

リスト3 変数の整列、布置の描画スクリプト(mymodule.py

 

import copy

import numpy as np

import matplotlib.pyplot as plt

 

 

 

def arrng_pttn(names, A):

    v_nms = copy.copy(names)

    pttn = copy.copy(A)

    n = len(v_nms)

    q = len(pttn[0])

    vals = np.empty(n)

    for i in range(n):

        mx_p = np.argsort(-np.abs(pttn[i]))

        vals[i] = np.abs(pttn[i][mx_p[0]]) * (0.1**mx_p[0])

    idx = np.argsort(-vals)

    return v_nms[idx], pttn[idx]

 

 

 

def draw_map(names, xcoord, ycoord, stitle, sxlabel, sylabel):

    plt.figure(figsize=(5, 5))

    plt.title(stitle, fontsize=14)

    plt.xticks([-1, 0, 1])

    plt.yticks([-1, 0, 1])

    plt.xlabel(sxlabel, fontsize=12)

    plt.ylabel(sylabel, fontsize=12)

    plt.plot([-1, 1], [0, 0], c='k', lw=1)

    plt.plot([0, 0], [-1, 1], c='k', lw=1)

    n = len(names)

    for i in range(n):

        plt.plot(xcoord[i], ycoord[i], 'bo', mfc='none')

    for i in range(n):

        plt.text(xcoord[i], ycoord[i], names[i], c='k')

 

    plt.tight_layout()

    plt.show()

 

 

 

 

 

Home