Post

주성분분석과 요인분석의 차이

주성분분석(Principal Component Analysis)과 요인분석(Factor Analysis)의 차이가 뭘까요? 예시로 R을 사용해서 포켓몬 능력치 데이터의 차원을 축소해보고(PCA), 씨리얼 평가의 특성을 구성하는 잠재적 요인을 찾아봅니다(FA).

고차원의 데이터일수록 표본의 밀도는 떨어지고, 높은 과대적합 위험과 계산 비용, 낮은 모델 성능 등 소위 차원의 저주 문제가 뒤따릅니다. 차원 축소의 중요성에 대해서는 길게 적지 않아도 많은 분들이 알고 계실 것이라고 생각합니다. 데이터의 차원을 축소하는 데는 다양한 방식이 있고, 변수 선택(feature selection)과 변수 추출(feature extraction)로 나뉠 수 있습니다. 전자는 주어진 많은 변수들 중 특정 기준에 따라, 혹은 주어진 태스크에 맞게 가장 중요한/관련 있는 변수들을 일부 선택하는 것이고, 후자는 기존 변수들을 조합하여 (일반적으로는 기존보다 수가 적은) 새로운 변수들을 만들어내는 것이죠.

변수 추출 중 가장 많이 알려진 방법 중 하나는 주성분분석(PCA, Principal Component Analysis) 입니다. PCA는 고차원 데이터를 그보다 수가 적지만 기존의 분산을 최대한 보존하는 선형 독립의 새로운 변수들로 변환합니다. 아이디어는 기본적으로 데이터에 가장 가까운 초평면을 정의하고, 기존 데이터를 이 평면에 투영하는데, 이 평면을 고를 때 분산이 최대로 보존하는 축을 고르자는 것입니다(분산이 최대인 축을 찾고, 데이터를 첫번째 축에 직교한 다음, 또 다시 반복).

PCA의 단계는 다음과 같이 요약됩니다.

  1. 공분산 행렬 계산하기
  2. 고유분해를 통해 고유값과 고유 벡터 계산하기
  3. 고유값이 큰 순서대로 나열하기
  4. 지정된 최소 분산 크기 이상을 설명하도록 n번째 고유 벡터까지 선택하기 (=이게 우리의 ‘주성분’입니다.)
  5. 기존 데이터에 선택된 고유벡터를 내적해주면 짠! 새로운 저차원의 데이터를 얻습니다.

이런 주성분분석과 매우 유사하게 생긴 다른 통계 기법이 하나 있는데요, 바로 요인분석(Factor Analysis) 입니다. 유사하게 생겼다는 말은 다음 그림을 보시면 됩니다.

https://user-images.githubusercontent.com/40485819/79882958-4e957080-842e-11ea-8be4-4fd041440a00.png (좌) PCA 도식 (우) 요인분석 도식 - 대충 비슷하게 생겼는데?

그림 상으로 바로 파악할 수 있는 공통점은 여러 개의 변수로부터 하나의 요인 혹은 성분을 얻는다는 것입니다(이름만 다른 거 아냐?). 즉 둘 다 분석을 하고 나면 다수의 변수들이 그보다 적은 수의 변수들로 줄어들게 됩니다(똑같이 차원 축소 아냐?).

그러나 요인분석은 차원 축소와는 다른 맥락에서 사용됩니다. 요인분석의 목적은 데이터의 여러 변수들이 가지고 있는 공분산 구조를 밝히는 것입니다. 다른 말로 하면, 수많은 변수가 있을 때 그들 사이에 어떤 관계가 있어서, 특정 변수가 다른 변수와 함께 변하고 그것은 그들을 공통적으로 설명하는 숨겨진 요인이 있기 때문이라는 가정 하에 그 요인을 찾고자 합니다.

이때 요인분석은 관찰 변수들이 영향을 받는 요인이 두 가지가 있다고 가정하는데, 바로 공통 요인(common factor = 우리가 구하려고 하는 숨겨진 그 요인)과 고유 요인(unique factor)입니다. 이 두 가지를 분리하여 구분한다는 것, 즉 우리가 찾고자 하는 요인 외에도 변수들에 영향을 미치는 오차를 고려한다는 것이 PCA와의 또다른 결정적인 차이입니다. 저 위 그림 상으로 제일 다른 부분은 바로 각 변수($Y_i$)에 영향을 주는 고유 요인($e_i$)의 존재인데요. 각 변수들에 미치는 영향 중 공통 요인과 관련 없고 각 변수들 자체와만 관련이 있는 고유한 분산을 뜻하며, 목적인 요인을 구할 때 얘네들은 분리되어야 합니다.

바로 이 오차에 대한 고려 때문에 요인분석은 PCA와 달리 상관행렬을 그대로 사용하지 않고, 수정된 버전(reduced correlation matrix)으로 바꾸어 사용합니다. 이 행렬에 대해 고유 분해를 하면 사전 요인 적재량을 얻을 수 있습니다. 이 단계까지가 PCA에서 고유벡터를 얻는 것과 동일한 것 같네요. 단 요인분석에서는 이걸 그냥 쓰진 않고요, 각 요인에 각 변수들이 명확하게 할당되어 해석되는 구조를 얻으려면 회전(rotation)이 필요합니다. 이후 단계에 대해서는 (이 글은 요인분석 기법에 대해 설명하는 것이 목적이 아니므로) 잠시 넘어가겠습니다. 자세한 내용은 나중에 더 쓸 기회가 있으면 좋겠네요.

제가 이해하기로 둘의 가장 큰 차이는 데이터에 대한 가정을 내리고 그를 검증하는 통계 모델인지의 여부입니다. 요인분석은 하나의 통계 모델로서 데이터에 대한 가정(특정 공분산 구조)을 수립하기 때문에 그에 따른 적합도(fit)을 보고 검증할 수 있습니다. 반면 PCA는 어떤 가정을 내리고 구조를 모델링한다기보다는 자료를 파악하거나 이후 진행될 작업의 성능과 효율을 높이기 위해 하기 위해 주어진 데이터를 최대한 보존하는 저차원의 데이터를 얻는 기법입니다.


요인분석 해보기

예시로 요인분석을 한 번 해보겠습니다. 요인분석하기 적당한 데이터를 검색해봤다가 씨리얼 데이터를 찾았습니다. 여러 종류의 씨리얼의 ‘여러 면’에 대해 평가한 데이터인데, 그 여러 면이라는 것은 바삭바삭하다, 달다, 건강하다, 심심하다, 에너지를 준다, 영양이 풍부하다, 어린이가 좋아할 것 같다, 가격이 합리적이다, … 등등입니다. 엄청 많죠? 흔히 제품 만족도를 조사하면 이런 식의 설문이 끝도 없이 이어지게 됩니다. 아마 다들 한번쯤은 접해보셨을 거예요. (예 - 다음과 같은 문장에 어느 정도 동의하십니까? 5점 척도로 답해주세요. 이 제품을 섭취하고 호랑이 기운이 솟아났다. 1: 전혀 아니다 ~ 5: 매우 그렇다.)

실제로 요인분석은 이런 검사나 평가 개발에 주로 사용됩니다. 아무튼 요인분석에서 우리가 하는 가정은 분명히 우리가 딱 볼 수 있는 관찰 변수인 저 많은 항목들에 공통적으로 영향을 주는 몇 개의 중요한 잠재적 특성이 있을 거야! 라는 것입니다. 그 잠재적 특성은 겉으로 드러나는 것은 아니지만, 공통적으로 해당 특성에 영향을 받는 변수들은 같이 움직인다는 것이죠(공분산). 예를 들면 ‘맛’이라는 큰 잠재 요인이 있어서 바삭바삭하다, 달다, 짜다, 심심하다, 과일 맛이다 등의 항목이 같이 묶일 거라고 예상하는 겁니다. 한번 볼까요?

오늘은 R을 써 보겠습니다. 우선 R은 내장 함수 factanal 로 탐색적 요인 분석을 지원합니다. 혹은 psych 패키지의 fa 함수를 쓰고, 회전을 위해 GPArotation 패키지를 이용해도 됩니다(관련 문서). 확인적 요인분석의 경우 공분산 구조에 기반한 전반적인 구조방정식 모델링 목적으로 만들어진 lavaan 을 사용할 수 있습니다(튜토리얼은 여기).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> head(cereal)
      Cereals Filling Natural Fibre Sweet Easy Salt Satisfying Energy Fun Kids Soggy Economical Health Family
1    Weetabix       5       5     5     1    2    1          5      4   1    4     5          5      5      5
2    SpecialK       1       2     2     1    5    2          5      1   1    5     3          5      2      5
3    SpecialK       5       4     5     5    5    3          5      5   5    5     3          3      5      5
4     CMuesli       5       5     5     3    5    2          5      5   5    5     3          3      5      5
5  CornFlakes       4       5     3     2    5    2          5      4   5    5     1          5      5      3
6 RiceBubbles       4       4     4     2    5    2          5      4   5    5     1          5      4      5
  Calories Plain Crisp Regular Sugar Fruit Process Quality Treat Boring Nutritious
1        1     3     1       4     1     1       3       5     1      1          5
2        1     5     5       1     2     1       5       2     1      1          3
3        1     1     5       4     3     1       2       5     4      1          5
4        1     1     1       4     2     5       2       5     5      1          5
5        3     1     5       3     1     1       3       5     5      1          4
6        3     1     5       3     2     1       3       5     5      1          4
> ncol(cereal)-1
[1] 25

씨리얼의 이름과 각 씨리얼에 대해 총 25개의 질문 응답이 있습니다. 다음과 같이 상관을 확인하면, 일부 변수들이 굉장히 강한 상관을 보인다는 사실을 확인할 수 있습니다(예 - FamilyKids )

https://user-images.githubusercontent.com/40485819/80305449-b285b380-87f7-11ea-8c52-9ad1ca47d592.png

우선 요인분석을 하기 전에, 요인 개수를 미리 지정해 줘야 합니다. 요인 개수를 파악하는 방법 중 하나는 다음과 같이 스크리 도표를 그려보는 것입니다.

1
> scree(cereal_answer)

https://user-images.githubusercontent.com/40485819/80305482-faa4d600-87f7-11ea-8777-bf9dbbd2d254.png

스크리 도표의 세로축은 각 요인이 설명하는 분산의 양을 나타내는 고유값(eigenvalue)입니다. 이 도표에서 요인들은 이 고유값이 큰 순서대로 나열되어 있으며, 가로축이 그 순서대로 n번째 요인임을 나타냅니다. 기본 아이디어는 어느 정도 이상의 분산을 설명하는, 즉 중요한 요인만 요인으로 채택하자는 것입니다. 보통 요인 개수는 고유값 1에서 자르거나(Kaiser’s rule), 직관적으로 그림 상으로 선이 급격하게 꺾여 평평해지는 지점에서 요인 개수를 채택하기도 합니다(elbow method). 다만 요인이 너무 많이 추출되는 경우를 방지하기 위해 각 요인에 할당되는 변수들이 너무 적지는 않은지, 전체 요인의 누적 분산 비율이 어떤지, 개별 요인의 해석 가능성은 어떠한지 이후 종합적으로 확인해 봐야 할 필요는 있습니다.

이 경우 요인 개수를 다섯개로 놓고 해 보겠습니다.

1
> fa(cereal_answer, nfactors = 5, fm = 'ml', rotate='varimax')

nfactors 인자는 요인 개수를 지정해주고, fm은 요인을 추출하는 방식, rotate 인자는 회전 방식을 설정하는 부분입니다. 요인 추출에는 주축분해법(PAF), 최대가능도법(ML) 등이 있습니다. 회전 방식은 크게 직각 회전(orthogonal rotation)과 사각 회전(oblique rotation)으로 나뉘며, 요인들 간의 상관이 존재한다고 가정할 경우 사각 회전을 사용합니다(사실 현실의 대부분의 데이터는 이 경우에 해당할 것입니다).

이제 결과를 확인해봅시다. 요인 적재량(factor loading)을 보면,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
> loadings(fa_result)

Loadings:
           ML1    ML2    ML3    ML4    ML5   
Filling     0.647         0.190  0.144  0.487
Natural     0.731 -0.215                0.153
Fibre       0.816                            
Sweet              0.696         0.351  0.166
Easy        0.230         0.307              
Salt               0.689                     
Satisfying  0.570         0.387  0.199  0.333
Energy      0.611         0.168  0.225  0.339
Fun         0.125  0.155  0.377  0.538       
Kids                      0.867              
Soggy                     0.130 -0.454       
Economical        -0.258  0.409 -0.197 -0.110
Health      0.840 -0.271                     
Family                    0.794  0.122       
Calories   -0.155  0.592         0.122  0.179
Plain      -0.115               -0.638 -0.150
Crisp              0.157  0.335  0.459       
Regular     0.657                            
Sugar      -0.177  0.852         0.170       
Fruit       0.341  0.161 -0.284  0.439  0.152
Process    -0.214  0.387        -0.101 -0.184
Quality     0.681 -0.222  0.200  0.218 -0.102
Treat       0.234  0.216  0.299  0.650       
Boring     -0.150        -0.198 -0.508       
Nutritious  0.849 -0.154

개인적으로 요인 분석의 재미는 묶인 변수들을 보고 해석을 한 뒤 이름을 짓는 부분이라고 생각합니다. 여기에 분석자의 주관적인 해석이 많이 들어가죠(고민하다가 좀 멋있고 딱 맞는 것 같은 이름을 짓는 데 성공하면 기분이 좋습니다). 예를 들어 요인3의 경우 Kids, Family, Economical 의 적재량이 높은 것으로 보아 가족 적합성 요인으로, 요인1 같은 경우는 Natural, Nutritious, Quality , Energy 를 보아 품질 요인으로, 요인4의 경우는 Fruit , Treat, Fun, Crisp 등을 보아 식감 요인으로 요인 이름을 지어 봅니다.

loadings() 호출시 가장 마지막에 나오는 것은 각 5개의 요인이 전체 분산을 얼만큼 설명하는지에 대한 비율입니다. 다 합하면 총 52% 정도이고, 이것이 전체 중 우리의 요인 모델이 설명하는 분산의 비율입니다. 아까 언급하였듯 요인분석은 공통 요인뿐 아니라 고유 분산, 즉 기존의 변수 각각의 오차의 존재도 인정하기 때문에 이 분산이 100%가 아니게 됩니다.

1
2
3
4
ML1   ML2   ML3   ML4   ML5
SS loadings    5.042 2.599 2.416 2.412 0.695
Proportion Var 0.202 0.104 0.097 0.096 0.028
Cumulative Var 0.202 0.306 0.402 0.499 0.527

~다음으로 넘어가기 전 잠깐~ 요인분석을 하면 결과가 어떤 식으로 나오는지 보이는 것을 목적으로 두고 일부 단계를 생략했음을 밝혀 둡니다. 요인분석은 다른 많은 분석 방법과 마찬가지로 실시하기 전에 데이터가 해당 기법에 적합한지 확인하는 것이 필요합니다(구형성 검정 등). 또 우리는 여기서 탐색적 요인분석(EFA, Explaratory Factor Analysis)을 진행했지만, 사실 요인분석의 진짜 목적인 ‘공분산 구조를 밝히는 것’의 가장 중요한 부분은 모든 모델링이 그러하듯 모델에 대한 ‘검증’입니다. 우리가 묶어놓은 변수들이 과연 제대로 잘 묶인 것인가? 우리가 만든 잠재 요인과 구조에 대한 가정은 과연 이 데이터와 얼마나 잘 맞을까? 그런 질문들에 대한 대답은 아직 안 된 것입니다. 또 어느 정도 구조에 대한 가설이 있다면, 확인적 요인분석(CFA, Confirmatory Factor Analysis)을 수행해야 합니다.


PCA 해보기

이제 다시 PCA로 돌아가 봅시다. PCA의 목표는 앞에서 언급했듯 차원 축소입니다. 주어진 데이터의 분산을 최대한 보존하면서 더 수는 적은 주성분 몇 가지를 얻는 것이죠. 요인분석에서 우리가 최종적으로 얻는 요인이 관찰 가능한 변수가 아니라 숨겨진 잠재적 특성이라고 했죠? PCA의 결과인 주성분은 관찰 가능한 데이터이며, (그걸 가지고 시각화 해서 살펴도 보고 분류 모델 만들어서 성능도 향상시키고… 대체로 그 뒤에 해야 할 일이 더 많죠.) 기존 변수들을 가지고 선형결합을 해서 새로운 변수를 만든 것입니다. 요인(잠재변수)로부터 관찰변수가 결정되는 요인분석과는 화살표의 방향이 다릅니다.

PCA도 한 번 해 봅니다. 이번 예시는 게임 시리즈 기반 포켓몬 능력치 데이터입니다. 총 721종이라고 하네요. 능력치는 다음과 같이 HP(체력), 공격, 방어, 특수공격(SP.ATK), 특수방어(SP.DEF), 속도로 이루어져 있습니다.

https://user-images.githubusercontent.com/40485819/80106493-e58c3500-85b4-11ea-82f5-16ea3cf9366d.jpg

데이터는 그 외에도 무슨 상성의 포켓몬인지, 몇 세대 포켓몬인인지, 전설의 포켓몬인지 여부도 포함하고 있지만 능력치 외의 부분은 제외합니다. (치코리타는 풀포켓몬이고, 전설의 포켓몬이 아니며, 2세대 포켓몬입니다. 그리고 귀엽습니다.)

일단 켰으니까 쭉 R을 써봅니다.

1
2
3
4
5
6
7
8
> head(pokemon)
  X.                  Name Type.1 Type.2 Total HP Attack Defense Sp..Atk Sp..Def Speed Generation Legendary
1  1             Bulbasaur  Grass Poison   318 45     49      49      65      65    45          1     False
2  2               Ivysaur  Grass Poison   405 60     62      63      80      80    60          1     False
3  3              Venusaur  Grass Poison   525 80     82      83     100     100    80          1     False
4  3 VenusaurMega Venusaur  Grass Poison   625 80    100     123     122     120    80          1     False
5  4            Charmander   Fire          309 39     52      43      60      50    65          1     False
6  5            Charmeleon   Fire          405 58     64      58      80      65    80          1     False

이 변수들의 상관관계는 다음과 같습니다.

https://user-images.githubusercontent.com/40485819/80224711-09b74700-8685-11ea-8db9-f55b728b5d48.png

6개는 뭐 그렇게 변수가 엄청 많은 건 아니지만, 우리는 포켓몬의 전반적인 강함을 나타내는 이 데이터를 조금 더 적은 수의 변수로 설명하고 싶습니다. 일부 변수들은 딱 보기에 어느 정도의 상관이 있으니까, 굳이 6개를 다 가져갈 필요는 없을 것 같거든요.

R에서 PCA를 할 때는

  • 내장 함수 prcomp
  • FactoMineR 패키지의 PCA
  • stats 패키지의 princomp

    등의 옵션이 있습니다. 이번에는 prcomp 함수를 쓰고, 몇 가지 시각화를 위해 factoextra 패키지도 함께 사용하겠습니다.

1
2
3
4
5
6
7
> pca_result <- prcomp(pokemon, scale=TRUE) #scale: 표준화를 해줘야 합니다.
> summary(pca_result)
Importance of components:
                          PC1    PC2    PC3    PC4     PC5     PC6
Standard deviation     1.6466 1.0457 0.8825 0.8489 0.65463 0.51681
Proportion of Variance 0.4519 0.1822 0.1298 0.1201 0.07142 0.04451
Cumulative Proportion  0.4519 0.6342 0.7640 0.8841 0.95549 1.00000

PCA 결과 요약에서 가장 중요한 것은 아마 각 주성분이 전체의 어느 정도를 설명하는지일 것입니다. 사용하는 도구에 따라 표시되는 방식은 다르겠지만, 대체로 맨 밑 줄의 누적 비율(Cumulative proportion)을 확인합니다. 첫번째 주성분까지 사용할 경우 전체 분산의 45%, 두번째까지는 63%, 이런 식으로 몇 개까지 성분을 고르면 전체 분산의 어느 정도를 설명하는지 확인할 수 있습니다. 예를 들어 80%에서 자르겠다! 라는 말은 차원을 축소하는 대신 나머지 20% 가량의 분산은 포기하겠다는 것입니다. 우리의 예제에서는 4개의 성분까지 고르면 전체의 대부분의 분산(88%)를 설명하는 것을 볼 수 있죠. 나머지 2차원 없이도 데이터의 분산이 어느 정도 보존된다는 것입니다.

여기서 요인분석과 또 다른 차이가 드러납니다. PCA에서는 결과적으로 1번째 성분이 제일 중요합니다. 변수들의 분산을 가장 많이 설명한다는 뜻에서 그렇습니다. PCA는 기본적으로 가장 분산이 큰 축을 찾고, 그것과 직교하는 - 즉 그것이 설명하는 부분을 제외하고 - 다음 축을 찾기 때문에 두번째 성분은 첫번째 성분보다 설명 분산이 크게 작아질 수밖에 없습니다. 반면 요인분석에서 아까 제가 이름 지어본 요인들 중 예컨대 식감 요인과 가족 적합성 요인은 그냥 다른 측면인 것이지 뭐가 더 중요하다 이런 건 없죠.

다음과 같이 biplot을 그려보면, 기존 6개의 변수들과 새로 만들어진 첫번째, 두번째 주성분의 관계를 시각적으로 확인할 수 있습니다.

1
biplot(pca_result, main="Biplot")

https://user-images.githubusercontent.com/40485819/80231490-666b2f80-868e-11ea-938c-0bc79b00a7a9.png

다음은 각 주성분에 대한 기존 변수의 기여도를 비교한 것입니다. factoextra 패키지의 fviz_contrib 함수를 통해 쉽게 시각화하여 비교해볼 수 있습니다. 4개 성분까지 쓰기로 하고 4개만 그려보겠습니다.

1
2
3
4
5
6
7
8
> pca_result$rotation
              PC1         PC2         PC3        PC4         PC5        PC6
HP      0.3898858  0.08483455 -0.47192614  0.7176913 -0.21999056  0.2336690
Attack  0.4392537 -0.01182493 -0.59415339 -0.4058359  0.19025457 -0.5029896
Defense 0.3637473  0.62878867  0.06933913 -0.4192373 -0.05903197  0.5368986
Sp..Atk 0.4571623 -0.30541446  0.30561186  0.1475166  0.73534497  0.2045304
Sp..Def 0.4485704  0.23909670  0.56559403  0.1854448 -0.30019970 -0.5451707
Speed   0.3354405 -0.66846305  0.07851327 -0.2971625 -0.53016082  0.2551400
1
dim_1st <-fviz_contrib(pca_result, "var",axes = 1)

https://user-images.githubusercontent.com/40485819/80231985-222c5f00-868f-11ea-8d6c-62b89c7254d7.png

가장 많은 분산을 설명하는 제1주성분에는 여섯 개의 능력치가 꽤 골고루 기여하네요. 반면 제2주성분의 경우 속도방어가 큰 비중을 차지하는 성분이죠. 제4성분의 경우 HP와 많이 관련이 있는 것 같습니다. 그러나 여기서 요인분석과 또 다른 차이: PCA에서는 얻은 결과를 통해 각 성분들이 어떤 의미를 가지는지 파악하는 것이 목적이 아니며, 사실 많은 경우 파악하려고 해도 잘 안 될 것입니다. 예컨대 여기서 우리는 제1성분이 대체 능력치의 어떤 부분을 뜻한다고 할 수 있는지 딱 잘라 말하기 어렵습니다. 그러나 요인분석에서는 각 요인이 어떤 잠재적 특성을 대표하는지 의미를 (일부 주관적으로나마) 해석하는 게 가능하며, 사실 이것이 요인분석의 목적이기도 합니다(그게 잘 안 되면, 즉 해석 가능한 요인으로 잘 묶이지 않으면 데이터가 적합하지 않거나 분석이 잘 안 된 것일 가능성이 큽니다).


마지막으로 정리하자면 둘 다 다차원의 자료로부터 그보다 적은 몇 개의 변수를 얻는다는 것이 공통점입니다. 차이점은 PCA의 목적은 차원 축소, FA의 목적은 변수 간 상관 구조를 설명하기 위해 공통 요인을 추출해내는 것입니다.

This post is licensed under CC BY 4.0 by the author.