エクセルで主成分分析をしたくて固有値・固有ベクトルを求めるマクロを作った.
標準モジュール
グローバル変数
Private x As SymmetryMatrix '最初は元のデータ行列。最終的に対角行列になる。 Private u As Matrix '固有ベクトルを並べた直交行列 Const CONVERGENCE_CRITERIA As Double = 10 ^ -20 '収束判定基準 Const MAX_ITERATION As Long = 1000 '最大反復回数
ワークシート関数
固有値を並べたベクトルを出力する関数EigenValue
Function EigenValue(r As Range) As Double() Call EigenValueDecomposition(r) EigenValue = x.Diag End Function
固有ベクトルを並べた直交行列を出力する関数EigenVector
Function EigenVector(r As Range) As Double() Call EigenValueDecomposition(r) EigenVector = u.ToArray End Function
メイン関数
Private Sub EigenValueDecomposition(r As Range) Set x = New SymmetryMatrix x.SetFromRange r Set u = New Matrix u.SetAsUnitMatrix x.Size Dim isConverge As Boolean, i As Long isConverge = False i = 0 Do Until isConverge Or i >= MAX_ITERATION i = i + 1 Dim g As GivensMatrix Set g = New GivensMatrix g.SetFromMaxOfUpperTriangularComponent x Set x = x.Rotate(g) 'x -> gxg Set u = u.Times(g) isConverge = (Abs(g.Theta) < CONVERGENCE_CRITERIA) Loop Call SortEigenvalue(x, u) End Sub
ヤコビ法は固有値が大きい順にならないのでクイックソートで整頓する.
Private Sub SortEigenvalue(y As SymmetryMatrix, s As Matrix, _ Optional firstIndex As Long = 0, Optional lastIndex As Long = -1) 'xとrを固有値の項順にソート '// ソート終了位置省略(最初の呼び出し)時は配列要素数を設定 If lastIndex = -1 Then lastIndex = y.Size - 1 '// 中央値を取得 Dim medianIndex As Long, median As Double medianIndex = CLng((firstIndex + lastIndex) / 2) median = y.Value(medianIndex, medianIndex) Dim leftIndex As Long, rightIndex As Long leftIndex = firstIndex '// 左ループカウンタ rightIndex = lastIndex '// 右ループカウンタ Do '// 中央値の左側をループし,配列の左側から中央値より小さい値を探す Do Until y.Value(leftIndex, leftIndex) <= median leftIndex = leftIndex + 1 Loop '// 中央値の右側をループし,配列の右側から中央値より大きい値を探す Do Until y.Value(rightIndex, rightIndex) >= median rightIndex = rightIndex - 1 Loop If leftIndex >= rightIndex Then '// 左側の方が大きければここで処理終了 Exit Do Else '// 右側の方が大きい場合は、 Call 左右を入れ替える(y, s, leftIndex, rightIndex) leftIndex = leftIndex + 1 '// 左側を1つ右にずらす rightIndex = rightIndex - 1 '// 右側を1つ左にずらす End If Loop If firstIndex < leftIndex - 1 Then Call SortEigenvalue(y, s, firstIndex, leftIndex - 1) '// 中央値の左側を再帰でクイックソート End If If rightIndex + 1 < lastIndex Then Call SortEigenvalue(y, s, rightIndex + 1, lastIndex) '// 中央値の右側を再帰でクイックソート End If End Sub
Private Sub 左右を入れ替える(y As SymmetryMatrix, s As Matrix, leftIndex As Long, rightIndex As Long) Dim temp As Double 'Call swap(y.Value(leftIndex, leftIndex), y.Value(rightIndex, rightIndex)) temp = y.Value(leftIndex, leftIndex) y.Value(leftIndex, leftIndex) = y.Value(rightIndex, rightIndex) y.Value(rightIndex, rightIndex) = temp Dim i As Long For i = 0 To y.Size - 1 'Call swap(s.Value(i, leftIndex), s.Value(i, rightIndex)) temp = s.Value(i, leftIndex) s.Value(i, leftIndex) = s.Value(i, rightIndex) s.Value(i, rightIndex) = temp Next End Sub
クラスモジュール Matrix
Private n_ As Long Private r_() As Double
Sub SetAsUnitMatrix(n As Long) n_ = n Dim result() As Double ReDim result(n - 1, n - 1) Dim i As Long, j As Long For i = 0 To n - 1 For j = 0 To n - 1 result(i, j) = IIf(i = j, 1, 0) Next j, i r_ = result End Sub
Property Get Size() As Long Size = n_ End Property Property Let Size(n As Long) n_ = n ReDim r_(n - 1, n - 1) End Property
Property Get Value(i As Long, j As Long) As Double Value = r_(i, j) End Property Property Let Value(i As Long, j As Long, v As Double) r_(i, j) = v End Property
Function ToArray() As Double() Dim result() As Double ReDim result(n_ - 1, n_ - 1) Dim i As Long, j As Long For i = 0 To n_ - 1 For j = 0 To n_ - 1 result(i, j) = r_(i, j) Next j, i ToArray = result End Function
Function ToSymmetryMatrix() As SymmetryMatrix Dim result As SymmetryMatrix Set result = New SymmetryMatrix result.Size = n_ Dim i As Long, j As Long For i = 0 To n_ - 1 For j = i To n_ - 1 result.Value(i, j) = (r_(i, j) + r_(j, i)) / 2 Next j, i Set ToSymmetryMatrix = result End Function
Function Times(g As GivensMatrix) As Matrix 'r -> r*g Dim rg As Matrix Set rg = Me.Clone Dim i As Long For i = 0 To n_ - 1 rg.Value(i, g.Row) = r_(i, g.Row) * g.RowRow + r_(i, g.Column) * g.ColumnRow rg.Value(i, g.Column) = r_(i, g.Row) * g.RowColumn + r_(i, g.Column) * g.ColumnColumn Next i Set Times = rg End Function
Function Clone() As Matrix Dim result As Matrix Set result = New Matrix result.Size = n_ Dim i As Long, j As Long For i = 0 To n_ - 1 For j = 0 To n_ - 1 result.Value(i, j) = r_(i, j) Next j, i Set Clone = result End Function
クラスモジュール SymmetryMatrix
仕様
- 入力が正方行列でない場合は行数・列数の小さいほうを大きさとする
- x_(i,j)はi<=jのときのみ定義することで強制的に対称化する
- 入力が対称行列でない場合は平均をとることで強制的に対称化する
Private n_ As Long Private x_() As Double
Sub SetFromRange(r As Range) n_ = IIf(r.Rows.Count < r.Columns.Count, r.Rows.Count, r.Columns.Count) Dim result() As Double ReDim result(n_ - 1, n_ - 1) Dim i As Long, j As Long For i = 0 To n_ - 1 For j = i To n_ - 1 result(i, j) = (CDbl(r(i + 1, j + 1)) + CDbl(r(j + 1, i + 1))) / 2 Next j, i For i = 1 To n_ - 1 For j = 0 To i - 1 result(i, j) = 0 Next j, i x_ = result End Sub
Property Let Size(n As Long) n_ = n ReDim x_(n - 1, n - 1) End Property Property Get Size() As Long Size = n_ End Property
Property Let Value(i As Long, j As Long, v As Double) If i <= j Then x_(i, j) = v Else x_(j, i) = v End Property Property Get Value(i As Long, j As Long) As Double Value = IIf(i <= j, x_(i, j), x_(j, i)) End Property
Property Get Diag() As Double() '固有値を並べた行ベクトル Dim result() As Double ReDim result(n_ - 1) Dim i As Long For i = 0 To n_ - 1 result(i) = x_(i, i) Next Diag = result End Property
Function ToMatrix() As Matrix Dim result As Matrix Set result = New Matrix result.Size = n_ Dim i As Long, j As Long For i = 0 To n_ - 1 For j = 0 To n_ - 1 result.Value(i, j) = Me.Value(i, j) Next j, i Set ToMatrix = result End Function
Function Times(g As GivensMatrix) As Matrix 'x -> xg Dim xg As Matrix Set xg = Me.ToMatrix Dim i As Long For i = 0 To n_ - 1 xg.Value(i, g.Row) = Me.Value(i, g.Row) * g.RowRow + Me.Value(i, g.Column) * g.ColumnRow xg.Value(i, g.Column) = Me.Value(i, g.Row) * g.RowColumn + Me.Value(i, g.Column) * g.ColumnColumn Next Set Times = xg End Function
Function Rotate(g As GivensMatrix) As SymmetryMatrix 'x -> gxg Set Rotate = g.Transpose.Times(Me.Times(g)).ToSymmetryMatrix End Function
クラスモジュール GivensMatrix
みたいな形の行列.
あえて1本の式で書くと
Private row_ As Long Private col_ As Long Private theta_ As Double
Sub SetFromMaxOfUpperTriangularComponent(x As SymmetryMatrix) Dim maxValue As Double row_ = 0 col_ = 1 maxValue = x.Value(0, 1) Dim i As Long, j As Long For i = 0 To x.Size - 2 For j = i + 1 To x.Size - 1 If Abs(x.Value(i, j)) > Abs(maxValue) Then row_ = i col_ = j maxValue = x.Value(i, j) End If Next j, i If x.Value(row_, row_) - x.Value(col_, col_) <> 0 Then Dim tan2t As Double tan2t = 2 * maxValue / (x.Value(row_, row_) - x.Value(col_, col_)) theta_ = Atn(tan2t) / 2 '確認:Atnの値域は-pi/2~pi/2 Else theta_ = Atn(1) * Sgn(maxValue) End If End Sub
Property Get Row() As Long Row = row_ End Property Property Let Row(i As Long) row_ = i End Property
Property Get Column() As Long Column = col_ End Property Property Let Column(j As Long) col_ = j End Property
Property Get Theta() As Double Theta = theta_ End Property Property Let Theta(t As Double) theta_ = t End Property
Property Get RowRow() As Double RowRow = cos(theta_) End Property Property Get ColumnColumn() As Double ColumnColumn = cos(theta_) End Property Property Get RowColumn() As Double RowColumn = -sin(theta_) End Property Property Get ColumnRow() As Double ColumnRow = sin(theta_) End Property
Function Times(x As Matrix) As Matrix 'gx Dim gx As Matrix Set gx = x.Clone Dim i As Long, j As Long For j = 0 To x.Size - 1 gx.Value(row_, j) = Me.RowRow * x.Value(row_, j) + Me.RowColumn * x.Value(col_, j) gx.Value(col_, j) = Me.ColumnRow * x.Value(row_, j) + Me.ColumnColumn * x.Value(col_, j) Next j Set Times = gx End Function
Function Clone() As GivensMatrix Dim result As GivensMatrix Set result = New GivensMatrix result.Row = row_ result.Column = col_ result.Theta = theta_ Set Clone = result End Function
Function Transpose() As GivensMatrix Dim result As GivensMatrix Set result = Me.Clone result.Theta = -theta_ Set Transpose = result End Function