Home

対応分析

Correspondence Analysis

 

クロス表の対応分析を、Greenacre (2021)に基づいて行うPythonスクリプトを書いてみた。

度数のクロス表が表1のように与えられたとする。

表1 度数のクロス表

 

 

 

 

列1

 

列j

 

列J

行1

 

 

 

 

 

 

 

 

i

 

 

 

 

 

 

 

 

行I

 

 

ここで、

とおく。

比率で表すと、表2のようになる。

表2 比率のクロス表

 

 

 

 

 

列1

 

列j

 

列J

行和

行1

 

 

 

 

 

 

 

 

 

i

 

 

 

 

 

 

 

 

 

行I

 

 

列和

 

 

1

 

表1の度数のクロス表を

表2の比率のクロス表を

と行列で表せば、

である。

行項目のプロファイルは

で表されるが、これは次元空間の点である。行項目の関係は、次元空間の布置として表されている。これを低次元空間に射影して、例えば2次元空間に写せば、平面上に布置を描くことができる。空間における点の布置を低次元空間への射影で近似する方法として特異値分解がある。

まず、の表す布置の中心が原点になるように次式による変換を行う。

ここで、

である。上式におけるは、1のみからなる縦ベクトルであるが、要素の数はそれぞれJとIで異なることに注意。

このを次式によりstandardizeする。

ここで、

である。

布置に対して低次元への射影を特異値分解(SVD)により求める。特異値分解を

と表す。ここで、

である。1次元空間への射影は、{}によって与えられ、2次元空間への射影は{}によって与えられ、3次元空間への射影は{}によって与えられ、・・・というように部分空間への射影は入れ子(nest)になっている。

行項目の点を射影した座標はで与えられるが、標準座標(standard coordinate)は

で与えられる。これに対して、特異値でスケールした

は主座標(principal coordinate)と呼ばれている。

次式が成り立つ。

ここで、

とおいた。

 

列項目も同様に考える。

列項目のプロファイルは

で表されるが、これは次元空間の点である。列項目の関係は、次元空間の布置として表されている。これを低次元空間に射影して、例えば2次元空間に写せば、平面上に布置を描くことができる。空間における点の布置を低次元空間への射影で近似する方法として特異値分解がある。

まず、の表す布置の中心が原点になるように次式による変換を行う。

このを次式によりstandardizeする。

布置に対して低次元への射影を特異値分解(SVD)により求める。特異値分解を

と表す。

列項目の点を射影した座標はで与えられるが、標準座標(standard coordinate)は

で与えられる。これに対して、特異値でスケールした

は主座標(principal coordinate)と呼ばれている。

次式が成り立つ。

 

次式の値はtotal inertiaと呼ばれている。

 

いま、

とおく。

であるので、

より、

すなわち、

式(1)の値は、the k-th principal inertiaと呼ばれている。

 

Inertiaについて行項目の観点から説明したが、列項目の観点からも同様に論じられる。

 

上の説明に基づいて対応分析のPythonスクリプトをリスト1のように作成した。スクリプトファイルとサンプルデータファイルは、まとめて圧縮ファイルcorresanalgrnacrfiles.zipとした。ダウンロード展開すれば自由に利用できる。

リスト1のスクリプトを実行すると、入力データファイル名の設定が求められる。

 

= RESTART: D:\・・・\corresanalgrnacrfiles\CorresAnal.py

Data File Name (*.xlsx) =

 

入力データファイルはExcelファイルの形式で用意する。図1は、政府統計ポータルサイトe-Statからダウンロードした「学校における教育の情報化の実態等に関する調査(令和3年度)」のファイルをリスト1のスクリプト用に編集したものである。

図1 「学校における教育の情報化の実態等に関する調査(令和3年度)」(e-Statより)を編集

 

1行目に列項目名、1列目に行項目名が設定されている。セル内の数値はPC(デバイス)の設置台数である。

図1のファイル名情報調査結果.xlsxを設定する。

 

Data File Name (*.xlsx) = 情報調査結果.xlsx

 

ファイル名を設定して、Enterキーを押すと計算が始まる。

計算が進むと、まずscree plot(図2)が表示される。

図2

 

第1軸でデータのかなりの部分が説明され、2軸もデータの傾向を若干表していることが読み取れる。

図2のフォームを閉じると、図3のフォームが提示される。

図3

 

行項目をprincipal coordinaesで、列項目をstandard coordinatesで表示したasymmetric mapである。

図3のフォームを閉じると、端末画面に次のメッセージが表示される。

 

Row items/principal coordinates: Column items/standard coordinates

x-axis =

 

次に描く図3のようなマップで用いる横軸の次元の設定が求められている。横軸と縦軸の組み合わせを変えて表示したいときはそれぞれに次元を順番に設定していけばよいが、0以下の値(整数値)を設定すると、図3の形式のマップの表示は終わり、次の形式のマップが表示される(図4)。

図4

 

列項目がprincipal coordinaesで、行項目をstandard coordinatesで表示されたasymmetric mapである。

図4のフォームを閉じると、端末画面に次のメッセージが表示される。

 

Row items/standard coordinates: Column items/principal coordinates

x-axis =

 

横軸と縦軸の次元を変えて図4の形式のマップを表示するときは、それぞれの次元を設定すればよい。次の形式のマップ(図5)に進むときは、0以下の整数値を設定する。

図5

 

図5は、列項目、行項目ともにprincipal coordinatessymmetric mapである。図1のデータに認められる「特別支援学校ではiOSまたはiPadの度数が多いこと」、「小学校・中学校ではChrome OSが多いこと」、「高等学校・高等学校(学科設置校)ではWindowsが主流であること」に対応した布置が見られる。

図5のフォームを閉じると、次のメッセージが表示される。

 

Row and Column items/principal coordinates

x-axis =

 

次元の組み合わせを変えて図5の形式のマップを表示するときは、次元を設定する。図5の形式のマップの表示を終了するときは、0以下の整数を設定する。

0以下の整数値を設定すると、プログラムの実行終了となり、端末には以下のメッセージが表示される。

 

Row and Column items/principal coordinates

x-axis = 0

Results.txt was saved.

 

出力ファイルResults.txtの内容は、以下の通りである。

 

Input data file = 情報調査結果.xlsx

 

Data =

    Unnamed: 0  Windows 11  Windows 10  ...  Chrome OS  Android  その他のOSLinux等)

0          小学校      105148     2306588  ...    2560813     4656            1780

1          中学校       56976     1271639  ...    1404207     3385             529

2       義務教育学校         527       28248  ...      18223      432               1

3         高等学校       58870      702666  ...     424957    10796            2629

4  高等学校(学科設置校)       40230      447571  ...     228314     5386            1934

5       中等教育学校         369       12311  ...       6250       74              20

6       特別支援学校        2705       48263  ...       4158      496             604

 

[7 rows x 10 columns]

 

Principal inertias

     Axis    Principal inertia   % inertia   Cumulative %

         1             0.05869        77.7           77.7

         2             0.01603        21.2           98.9

         3             0.00041         0.5           99.5

         4             0.00028         0.4           99.8

         5             0.00012         0.2          100.0

         6             0.00001         0.0          100.0

     Total             0.07555

 

Data =」に続く、読み込まれたデータの内容の表示のフォーマットに乱れが見られるのは、Pythonの日本語を含む文字列の書式制御がうまくいかないためである。内容に問題は無い。

 

 

参考文献

 

Greenacre, M. (1984). Theory and Applications of Correspondence Analysis. Academic Press

Greenacre, M. (2021). Correspondence Analysis in Practice, third edition (paperback). CRC Press.

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

 

 

 

リスト1 Pythonスクリプト

 

import pandas as pd

import numpy as np

import matplotlib.pyplot as plt

import japanize_matplotlib

 

flnm = input('Data File Name (*.xlsx) = ')

xlsx = pd.ExcelFile(flnm)              

data = pd.read_excel(xlsx)     

 

fout = open('Results.txt', 'w')

fout.write(f'Input data file = {flnm}\n')

 

print(data)

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

 

N = data.values

row_names = N[:,0]

col_names = data.keys()[1:]

N = N[:,1:]

sum_row = np.sum(N, axis=1)

sum_col = np.sum(N, axis=0)

n = np.sum(N)

P = N/n

I, J = np.shape(P)

print('\nP =')

for i in range(I):

    for j in range(J):

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

    print()

print()

   

r = P @ np.ones((J, 1))

c = P.T @ np.ones((I, 1))

print('r =')

for v in r:

    print(f'{v[0]:>10.4f}')

   

print('\nc =')

for v in c:

    print(f'{v[0]:>10.4f}')

   

Dr = np.diag(r[:,0])

Dc = np.diag(c[:,0])

 

Dr_n5 = np.diag(r[:,0]**-0.5)

Dc_n5 = np.diag(c[:,0]**-0.5)

S = Dr_n5 @ (P - r @ c.T) @ Dc_n5

S = np.array(S, dtype='float')

 

U, Da, Vh = np.linalg.svd(S, full_matrices = False)

ck = [1 if v > 1.0e-6 else 0 for v in Da]

K = sum(ck)

print('\nK =', K)

U = U[:,:K]

Da = np.diag(Da[:K])

V = Vh[:K].T

 

Phi = Dr_n5 @ U

Gmm = Dc_n5 @ V

 

print('\nPhi =')

for i, v in zip(row_names, Phi):

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

    for v1 in v:

        print(f'{v1:>10.4f}', end = '')

    print()

 

print('\nGmm =\n')

for i, v in zip(col_names, Gmm):

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

    for v1 in v:

        print(f'{v1:>10.4f}', end = '')

    print()

       

F = Phi @ Da

G = Gmm @ Da

print('\nF =')

for i, v in zip(row_names, F):

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

    for v1 in v:

        print(f'{v1:>10.4f}', end = '')

    print()

   

print('\nG =')

for i, v in zip(col_names, G):

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

    for v1 in v:

        print(f'{v1:>10.4f}', end = '')

    print()

   

Dlmbd = Da @ Da

 

alphas = np.diag(Da)

lambdas = np.diag(Dlmbd)

cum_lambdas = lambdas.cumsum()

pcum_lambdas = cum_lambdas / cum_lambdas[-1]

p_lambdas = lambdas / cum_lambdas[-1]

 

print('\nPrincipal inertias')

print(f'{"Axis ":>10s}{"Principal inertia":>20s}{"% inertia":>12s}{"Cumulative %":>15s}')

for i in range(K):

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

    print(f'{lambdas[i]:>20.5f}', end = '')

    print(f'{p_lambdas[i]*100:>12.1f}', end = '')

    print(f'{pcum_lambdas[i]*100:>15.1f}')

print(f'{"Total":>10s}{cum_lambdas[-1]:>20.5f}' )

print()

 

fout.write('\nPrincipal inertias\n')

fout.write(f'{"Axis ":>10s}{"Principal inertia":>20s}' +\

           f'{"% inertia":>12s}{"Cumulative %":>15s}\n')

for i in range(K):

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

    fout.write(f'{lambdas[i]:>20.5f}')

    fout.write(f'{p_lambdas[i]*100:>12.1f}')

    fout.write(f'{pcum_lambdas[i]*100:>15.1f}\n')

fout.write(f'{"Total":>10s}{cum_lambdas[-1]:>20.5f}\n' )

 

plt.bar(np.arange(K),lambdas)

plt.xticks(np.arange(K), np.arange(K)+1)

plt.xlabel('Axis', fontsize = 18)

plt.ylabel('Principal inertia', fontsize = 18)

plt.tight_layout()

plt.show()

 

#

#            Row items/principal coordinates

#            Column items/standard coordinates

#

dim1 = 1

dim2 = 2 if K > 1 else 1

while True:

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

    plt.title('Row items: Principal coordinates\n' +

             'Column items: Standard coordinates')

    for i in range(I):

        plt.plot(F[i][dim1-1], F[i][dim2-1], 'o', c = 'g')

        plt.text(F[i][dim1-1], F[i][dim2-1], row_names[i])

    for j in range(J):

        plt.plot(Gmm[j][dim1-1], Gmm[j][dim2-1], '^', c = 'orange')

        plt.text(Gmm[j][dim1-1], Gmm[j][dim2-1], col_names[j])

    v1 = np.max([F[:,dim1-1], F[:,dim2-1]])

    v2 = np.max([-F[:,dim1-1], -F[:,dim2-1]])

    v3 = np.max([Gmm[:,dim1-1], Gmm[:,dim2-1]])

    v4 = np.max([-Gmm[:,dim1-1], -Gmm[:,dim2-1]])

    max_v = np.max([v1, v2, v3, v4])

    plt.plot([-max_v, max_v], [0, 0], ls = ':', c = 'y')

    plt.plot([0, 0], [-max_v, max_v], ls = ':', c = 'y')

    plt.xlabel(f'Axis {dim1}: {lambdas[dim1-1]:.4f} ({100*p_lambdas[dim1-1]:.1f}%)')

    plt.ylabel(f'Axis {dim2}: {lambdas[dim2-1]:.4f} ({100*p_lambdas[dim2-1]:.1f}%)')

    plt.show()

    print('Row items/principal coordinates: Column items/standard coordinates')

    dim1 = int(input('x-axis = '))

    if dim1 < 1:

        break

    if dim1 > K:

        dim1 = K

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

    if dim2 < 1:

        dim2 = 1

    if dim2 > K:

        dim2 = K

 

#

#        Row items/standard coordinates 

#        Column items/principal coordinates

#

dim1 = 1

dim2 = 2 if K > 1 else 1

while True:

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

    plt.title('Row items: Standard coordinates\n' +

             'Column items: Principal coordinates')

    for i in range(I):

        plt.plot(Phi[i][dim1-1], Phi[i][dim2-1], 'o', c = 'g')

        plt.text(Phi[i][dim1-1], Phi[i][dim2-1], row_names[i])

    for j in range(J):

        plt.plot(G[j][dim1-1], G[j][dim2-1], '^', c = 'orange')

        plt.text(G[j][dim1-1], G[j][dim2-1], col_names[j])

    v1 = np.max([Phi[:,dim1-1], Phi[:,dim2-1]])

    v2 = np.max([-Phi[:,dim1-1], -Phi[:,dim2-1]])

    v3 = np.max([G[:,dim1-1], G[:,dim2-1]])

    v4 = np.max([-G[:,dim1-1], -G[:,dim2-1]])

    #print('v4 =', v4)

    #print(-Gmm[:,0:1])

    max_v = np.max([v1, v2, v3, v4])

    plt.plot([-max_v, max_v], [0, 0], ls = ':', c = 'y')

    plt.plot([0, 0], [-max_v, max_v], ls = ':', c = 'y')

    plt.xlabel(f'Axis {dim1}: {lambdas[dim1-1]:.4f} ({100*p_lambdas[dim1-1]:.1f}%)')

    plt.ylabel(f'Axis {dim2}: {lambdas[dim2-1]:.4f} ({100*p_lambdas[dim2-1]:.1f}%)')

    plt.show()

    print('Row items/standard coordinates: Column items/principal coordinates')

    dim1 = int(input('x-axis = '))

    if dim1 < 1:

        break

    if dim1 > K:

        dim1 = K

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

    if dim2 < 1:

        dim2 = 1

    if dim2 > K:

        dim2 = K

 

#

#                 Symmetric Map

#

dim1 = 1

dim2 = 2 if K > 1 else 1

while True:

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

    plt.title('Row items: Principal coordinates\n' +

             'Column items: Principal coordinates')

    for i in range(I):

        plt.plot(F[i][dim1-1], F[i][dim2-1], 'o', c = 'g')

        plt.text(F[i][dim1-1], F[i][dim2-1], row_names[i])

    for j in range(J):

        plt.plot(G[j][dim1-1], G[j][dim2-1], '^', c = 'orange')

        plt.text(G[j][dim1-1], G[j][dim2-1], col_names[j])

    v1 = np.max([F[:,dim1-1], F[:,dim2-1]])

    v2 = np.max([-F[:,dim1-1], -F[:,dim2-1]])

    v3 = np.max([G[:,dim1-1], G[:,dim2-1]])

    v4 = np.max([-G[:,dim1-1], -G[:,dim2-1]])

    max_v = np.max([v1, v2, v3, v4])

    plt.plot([-max_v, max_v], [0, 0], ls = ':', c = 'y')

    plt.plot([0, 0], [-max_v, max_v], ls = ':', c = 'y')

    plt.xlabel(f'Axis {dim1}: {lambdas[dim1-1]:.4f} ({100*p_lambdas[dim1-1]:.1f}%)')

    plt.ylabel(f'Axis {dim2}: {lambdas[dim2-1]:.4f} ({100*p_lambdas[dim2-1]:.1f}%)')

    plt.show()

    print('Row and Column items/principal coordinates')

    dim1 = int(input('x-axis = '))

    if dim1 < 1:

        break

    if dim1 > K:

        dim1 = K

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

    if dim2 < 1:

        dim2 = 1

    if dim2 > K:

        dim2 = K

 

fout.close()

print('Results.txt was saved.')

 

 

 

Home