나의 작은 연구 기록

[장문 주의]Big 5의 30개 하위 척도 Z점수 기반으로 상위 요인의 Z점수 그리고 RMSD 구하는 Python 소스 코드를 만들어 봅니다.

lamp2357 2026. 7. 10. 00:28

Big 5(NEO-PI-R)는 간단하게 설명하면 심리학자 폴 코스타(Paul Costa Jr.)와 로버트 매크레이(Robert R. McCrae)가 개발한 성격 스펙트럼 테스트(MBTI처럼 유형 테스트 아닙니다!)입니다.

두문자어 빌려서 OCEAN 혹은 SLOAN이라고도 부릅니다.

 

원본 논문을 보고 싶다면 여기에 들어가셔서 다운로드 받으시면 됩니다.

 

성격 5가지 상위 요인은 다음과 같습니다.

1. 개방성(O, Openness to Experience)

2. 성실성(C, Conscientiousness)

3. 외향성(E, Extraversion)

4. 우호성(A, Agreeableness)

5. 신경성(N, Neuroticism)

 

원본 논문에 존재하는 글들을 근거로 두어서 5가지 상위 요인들을 워크넷 정의(직업 선호도 검사 L형) 그대로 가져오면 다음과 같습니다.

[개방성]

- 개정 전 : 광범위한 주제에서 "새로운 것"에 대해 개인이 판단하는 경향을 확인하는 요인

- 개정 후 : 자기자신을 둘러싼 세계에 관한 관심, 호기심, 다양한 경험에 대한 추구 및 포용력 정도

근거 : "According to adjective-factor results, openness is best characterized by original, imaginative, broad interests, and daring."

[성실성]

- 개정 전 : 개인의 조직화된 정도를 확인하는 요인

- 개정 후 : 사회적 규칙, 규범, 원칙들을 기꺼이 지키려는 정도

근거 : "In addition to conscientious and scrupulous, there are a number of adjectives that suggest a more proactive stance: hardworking, ambitious, energetic, persevering."

[외향성]

- 개정 전 : 개인이 열정적으로 타인을 찾고 환경과 상호작용하는 것을 확인하는 요인

- 개정 후 : 타인과의 상호작용을 원하고 타인의 관심을 끌고자 하는 정도

근거 : "Extraversion or surgency. Sociable, fun-loving, affectionate, friendly, and talkative are the highest loading variables on the extraversion factor."

[우호성]

- 개정 전 : 대인 관계에서 보이는 질적인 측면을 확인하는 요인

- 개정 후 : 타인과 편안하고 조화로운 관계를 유지하는 정도

근거 : "The essential nature of agreeableness-antagonism is perhaps best seen by examining the disagreeable pole, which we have labeled antagonism. As the high-loading adjectives in Table 3 and the items in Table 2 show, antagonistic people seem always to set themselves against others. Cognitively they are mistrustful and skeptical; affectively they are callous and unsympathetic; behaviorally they are uncooperative, stubborn, and rude."

- 주의사항 : 위 근거는 우호성 낮음을 기준으로 정의한 것입니다. 이 내용의 정반대가 우호성 높음입니다.

[신경성]

- 개정 전 : 개인이 일상 속에서 발생하는 힘든 경험들에 부정적 정서를 얼마나 자주 경험하는지를 확인하는 요인

- 개정 후 : 정서적으로 얼마나 안정되어 있고 자신이 세상을 얼마나 통제할 수 있으며, 세상을 위협적이지 않다고 생각하는 정도

근거 : "A provocative view of neuroticism is provided by Tellegen (in press), who views it as negative emotionality, the propensity to experience a variety of negative affects, such as anxiety, depression, anger, and embarrassment."

 

이번에는 각 상위 요인의 하위 척도 6개씩 알려드리도록 하겠습니다.

참고로 출처는 https://m.blog.naver.com/1strider/222992024911 <- 여기입니다.

 

💡 [O] 개방성 (Openness to Experience)

  • 상상력 (Imagination): 공상과 내면세계의 풍부함 / [높음] 창의적이고 몽상적임 / [낮음] 현실적이고 실용적임
  • 예술적 감수성 (Artistic Interests): 예술과 미적 가치에 대한 관심 / [높음] 예술을 깊이 음미함 / [낮음] 예술이나 미적 자극에 무관심함
  • 감정 존중 (Emotionality): 자신의 감정에 대한 수용력 / [높음] 감정 기복을 섬세하게 느낌 / [낮음] 감정에 무던하고 무감각함
  • 모험성 (Adventurousness): 새로운 경험과 변화에 대한 시도 / [높음] 낯선 환경과 도전을 즐김 / [낮음] 익숙한 루틴과 안정을 선호함
  • 지적 호기심 (Intellect): 복잡하고 추상적인 개념에 대한 흥미 / [높음] 지적 토론과 철학을 즐김 / [낮음] 단순하고 구체적인 사실을 선호함
  • 가치 진보성 (Liberalism): 기존 권위나 전통에 대한 도전 / [높음] 진보적이고 새로운 가치를 추구함 / [낮음] 보수적이고 전통과 규칙을 고수함

📊 [C] 성실성 (Conscientiousness)

  • 자신감 (Self-efficacy): 스스로의 문제 해결 능력에 대한 믿음 / [높음] 유능하고 효율적이라 느낌 / [낮음] 자신의 능력을 자주 의심함
  • 계획성 (Orderliness): 체계적이고 정리된 상태 / [높음] 꼼꼼하게 정돈하고 룰을 지킴 / [낮음] 무계획적이고 환경이 어수선함
  • 책임감 (Dutifulness): 의무와 윤리적 원칙의 준수 / [높음] 약속과 원칙을 철저히 지킴 / [낮음] 규칙에 얽매이는 것을 싫어함
  • 성취욕 (Achievement-striving): 목표 달성을 위한 야망 / [높음] 목표 지향적이고 워커홀릭 기질이 있음 / [낮음] 현재에 만족하며 뚜렷한 야망이 없음
  • 자제력 (Self-discipline): 유혹을 이기고 끝까지 과제를 해내는 힘 / [높음] 집중력이 강하고 끈기가 있음 / [낮음] 쉽게 산만해지고 일을 자주 미룸
  • 신중함 (Cautiousness): 행동하기 전 결과를 생각하는 정도 / [높음] 행동이 신중하고 조심스러움 / [낮음] 즉흥적이고 충동적으로 결정함

🔥 [E] 외향성 (Extraversion)

  • 친밀감 (Warmth): 타인에게 다가가는 따뜻함 / [높음] 다정하고 쉽게 친해짐 / [낮음] 격식을 차리고 심리적 거리를 둠
  • 사교성 (Gregariousness): 많은 사람과 어울리는 것을 즐김 / [높음] 군중 속에 있기를 좋아함 / [낮음] 소수와의 교류나 혼자만의 시간을 선호함
  • 리더십 (Assertiveness): 타인에게 영향력을 행사하려는 성향 / [높음] 주도적으로 나서고 지배적임 / [낮음] 뒤에서 관망하고 타인에게 주도권을 넘김
  • 활동성 (Activity): 일상생활의 에너지와 속도 / [높음] 항상 바쁘고 에너지가 넘침 / [낮음] 여유롭고 느긋한 생활 방식을 선호함
  • 흥미 추구 (Excitement-seeking): 강한 자극과 스릴을 원함 / [높음] 모험적이고 짜릿한 자극을 즐김 / [낮음] 조용하고 안전한 환경을 선호함
  • 명랑함 (Positive-emotions): 긍정적인 감정을 느끼는 빈도 / [높음] 쾌활하고 낙관적이며 잘 웃음 / [낮음] 감정 표현이 적고 차분하며 진지함

🤝 [A] 우호성 (Agreeableness)

  • 신뢰 (Trust): 타인의 선의를 믿는 정도 / [높음] 사람을 기본적으로 선하다고 믿음 / [낮음] 의심이 많고 타인의 의도를 경계함
  • 정직함 (Honesty): 자신의 의도를 투명하게 드러냄 / [높음] 솔직하고 꾸밈이 없음 / [낮음] 전략적이고 자신의 속마음을 잘 감춤
  • 이타주의 (Altruism): 타인을 돕고자 하는 헌신 / [높음] 타인을 돕는 일에 적극적이고 관대함 / [낮음] 자신의 이익을 우선하며 돕기를 꺼림
  • 협조성 (Cooperation): 갈등 상황에서 양보하는 태도 / [높음] 대립을 피하고 타협함 / [낮음] 경쟁적이고 자신의 주장을 끝까지 관철함
  • 겸손함 (Modesty): 자신을 낮추고 내세우지 않음 / [높음] 자신을 과시하지 않고 겸손함 / [낮음] 스스로의 우월감을 드러내고 자랑함
  • 공감력 (Sympathy): 타인의 고통에 대한 동정심 / [높음] 마음이 약하고 온정적임 / [낮음] 감정보다 객관적이고 냉철하게 판단함

⛈️ [N] 신경성 (Neuroticism)

  • 걱정 (Anxiety): 미래에 대한 두려움과 불안 / [높음] 쉽게 긴장하고 걱정이 많음 / [낮음] 평온하고 태평하게 상황을 넘김
  • 분노 (Anger): 좌절 상황에서의 적대감 / [높음] 작은 일에도 쉽게 짜증을 내고 화를 냄 / [낮음] 화를 잘 내지 않고 감정이 온화함
  • 우울 (Depression): 슬픔과 죄책감을 느끼는 빈도 / [높음] 쉽게 낙담하고 무기력해짐 / [낮음] 기분 침체가 적고 정서가 평탄함
  • 자의식 (Self-consciousness): 타인의 시선에 대한 예민함 / [높음] 시선을 심하게 의식하고 부끄러워함 / [낮음] 타인의 평가에 신경 쓰지 않고 당당함
  • 충동성 (Impulsiveness): 강렬한 충동과 욕구를 통제하는 능력 / [높음] 식욕, 물욕 등의 유혹에 쉽게 굴복함 / [낮음] 충동을 쉽게 참아내고 욕구를 통제함
  • 심약함 (Vulnerability): 스트레스와 압박감에 대처하는 능력 / [높음] 위기 상황에서 패닉에 빠지고 무너짐 / [낮음] 극도의 스트레스 상황에서도 침착함을 유지함

 

잠깐! 본격적으로 들어가기 전에 우선 표준편차(SD, Z점수) 범위가 통계적으로 어떤 의미를 갖는지 간단하게 짚고 넘어가겠습니다.

  • -2.5 SD 이하 : 규격 외
  • -2.5 SD 초과 ~ -1.5 SD 이하 : 매우 낮은 편
  • -1.5 SD 초과 ~ -0.5 SD 이하 : 낮은 편
  • -0.5 SD 초과 ~ +0.5 SD 미만 : 중간 범위 (놀랍게도 가장 인구가 많이 몰려있는 평범한 구간입니다.)
  • +0.5 SD 이상 ~ +1.5 SD 미만 : 높은 편
  • +1.5 SD 이상 ~ +2.5 SD 미만 : 매우 높은 편
  • +2.5 SD 이상 : 규격 외

각 하위 척도마다 해당 범위에 들어갈 때 뜻하는 심리적 의미가 다 다릅니다.

하지만 그 이상 깊이 들어가면 이는 Python 데이터 분석의 범위를 넘어선 순수 심리학의 영역이 되므로,

여기서는 '내 점수가 어느 정도의 위치에 있는지' 파악하는 수치적 범위만 알려주고 넘어가도록 하겠습니다.

 

 

여기서는 하위 척도의 Z점수(표준편차 SD 기준)를 바탕으로 각 상위 요인의 Z점수, 그리고 평균 제곱근 편차(RMSD, Root Mean Square Deviation)를 구하는 Python 소스 코드를 만들도록 하겠습니다.

 

우선 각 상위 요인마다 6개의 하위 척도 Z점수로 어떻게 연산해서 각 요인의 Z점수가 되는지 천천히 유도하겠습니다.

 

가장 먼저 크론바흐 알파 계수에 대해 간단하게 알아봅니다.

$$ \alpha = \frac{k \bar{c}}{\bar{v} + (k - 1)\bar{c}} $$

이게 위키백과에 존재하는 크론바흐 알파 계수 공식입니다.

이 계수는 단일 실행 신뢰도(즉, 고정된 시간에서 여러 항목에 대한 응답자의 신뢰도) 계수이며 보통 시간이 정해져 있는 자가 진단 테스트에서 많이 사용합니다.

그리고 MBTI 정식 검사이든 Big 5 테스트 등 단일 실행 테스트에서 유용하게 사용되는 테스트입니다.

여기서 v와 c는 다음과 같습니다.

v : 각 하위 척도(문항)들의 평균 분산 (Average variance)

c : 하위 척도들 간의 평균 공분산 (Average inter-item covariance)

$$ Z = \frac{X - \mu}{\sigma} $$

위의 Z점수 표준화에 의해 모든 하위 척도의 평균은 0이 되고 표준편차와 분산(표준편차의 제곱)은 무조건 1이 됩니다.

$$ \text{Var}(Z_i) = \sigma_{Z_i}^2 = 1 $$

따라서 6개의 하위 척도의 분산이 모두 1이므로 이들의 평균 분산 v 역시 1이 됩니다.

$$ \bar{v} = \frac{1 + 1 + 1 + 1 + 1 + 1}{6} = 1 $$

그리고 통계학에서 두 변수 X, Y의 공분산(Covariance)을 각 변수의 표준 편차로 나누는 것이 바로 상관 계수(r)입니다.

$$ r_{xy} = \frac{\text{Cov}(X, Y)}{\sigma_X \sigma_Y} $$

그런데 Z점수 표준화에 의해 두 변수 X, Y의 표준편차가 모두 1입니다.

그러므로 표준화된 데이터 간의 공분산은 그 자체로 피어슨 상관계수와 일치하게 됩니다!

$$ \text{Cov}(Z_i, Z_j) = \frac{\text{Cov}(y_i, y_j)}{1 \times 1} = r_{ij} $$

따라서 평균 공분산 c는 곧 평균 상관계수 r이 됩니다.

$$ \bar{c} = \bar{r} $$

아까 맨 처음 알파 계수 공식에서 v = 1, c = r, k = N(하위 척도 개수)을 대입해주세요.

$$ \alpha = \frac{N \cdot \bar{r}}{1 + (N - 1) \cdot \bar{r}} $$

놀라운 사실은 각 요인마다 요구되는 r 값이 서로 달라서 각각 다르게 연산된다는 것입니다.

그건 연구 기관 및 측정 대상들이나 측정 국가 등의 영향으로 인해 다른 r값이 적용되는 것으로 추측됩니다.

보통 학계에서는 r = 0.3 ~ 0.45 사이를 선호하며 여기서는 자유롭게 설정하도록 하겠습니다.

다만, 공식 메뉴얼에서 확인해보면 226~227페이지에 다음과 같은 내용이 들어있습니다.

이 내용에 의해 r 값이 정말 천차만별로 나오게 됩니다.

더보기

Internal consistencies of the 48-item domain scores are high. For exam ple, in an adult sample ( N = 635), coeffi cient alphas for Neuroticism (N), Extraversion (E), Openness to Experience (O), Agreeableness (A), and Conscientiousness (C) domain scores from the NEO-PI-R were .92, .89, .88, .90, and .91, respectively, for Form S and .93, .90, .88, .93, and .93 for Form R (McCrae, Martin, & Costa, 2005). Th e corresponding values for 14- to 20-year-olds ranged from .87 to .94 (McCrae, Costa, et al., 2005). Coeffi cient alphas for the eight-item facet scales are understandably lower; in the adult sample they ranged from .51 to .86 ( Mdn s = .75 for Form S, .78 for Form R); in the adolescent sample they ranged from .44 to .84 ( Mdn s = .73 for Form S, .75 for Form R). Internal consistencies below .70 are sometimes considered problematic, but the few NEO-PI-R facet scales with values lower than .70 have nevertheless shown evidence of heritability, cross-observer agreement, and longitudinal stability comparable to those of the more internally consistent facets (McCrae, Kurtz, Yamagata, & Terrac ciano, 2011). Internal consistencies for the fi ve 12-item Form S NEO-FFI-3 domain scales ranged from .71 to .87 for middle-school children, .72 to .83 for adolescents, and .79 to .86 for adults (McCrae & Costa, 2007). Robins, Fraley, Roberts, and Trzesniewski (2001) reported two-week retest reliabilities of .86 to .90 for the NEO-FFI scales. McCrae, Yik, Trapnell, Bond, and Paulhus (1998) reported two-year retest reliabilities for the full NEO-PI-R; coeffi cients for N, E, O, A, and C were .83, .91, .89, .87, and .88. Retest reliabilities for the 30 facet scales ranged from .64 to .86 ( Mdn = .79). McCrae, Kurtz, and colleagues (2011) reported one-week retest coeffi cients of .70 to .91 for the facets and .91 to .93 for the domains in a sample of 132 college students. Terracciano, Costa, and McCrae (2006) reported 10-year stability coeffi cients for the NEO-PI-R. Th e median value was .70 for facets and .81 for factors.

이제부터 상위 요인의 Z점수를 구하도록 하겠습니다.

$$ Z_{\text{Domain}} = \frac{\sum Z_k}{\text{SD}_{Composite}} $$

합산 오차를 보정하기 위해 위 식을 사용합니다.

여기서 표준화된 공간에서 합산 점수의 분산은 다음과 같습니다.

$$ \text{Var}_{Composite} = N + N(N-1)\bar{r} = N \left[ 1 + (N-1)\bar{r} \right] $$

표준화된 알파 계수에 다시 집중합니다.

$$ \alpha = \frac{N\bar{r}}{1 + (N-1)\bar{r}} $$

위 식의 분모를 좌변에 넘겨버리고 알파를 우변의 분모에 넘겨버립니다.

$$ 1 + (N-1)\bar{r} = \frac{N\bar{r}}{\alpha} $$

그리고 아까 전의 분산 공식에 대입하면 분산 구조가 알파와 상관 계수의 비율로 나타납니다.

$$ \text{Var}_{Composite} = N \cdot \left( \frac{N\bar{r}}{\alpha} \right) = \frac{N^2 \bar{r}}{\alpha} $$

알파 공식에서 상관 계수에 대하여 역산하여 상관 계수에 대한 식으로 바꿉니다.

$$ \bar{r} = \frac{\alpha}{N - (N-1)\alpha} $$

그리고 아까 전의 분산 공식에 상관 계수 식을 대입합니다.

$$ \text{Var}_{Composite} = \frac{N^2}{\alpha} \cdot \left( \frac{\alpha}{N - (N-1)\alpha} \right) = \frac{N^2}{N - (N-1)\alpha} $$

표준 편차는 분산의 제곱근이니 전체에 루트를 씌우면 최종 복합 표준편차가 나옵니다.

$$ \text{SD}{Composite} = \sqrt{\text{Var}{Composite}} = \frac{N}{\sqrt{N - (N-1)\alpha}} $$

아까 맨 처음의 Z_Domain 공식 기억나시죠?

여기에 SD_Composite 식을 대입해줍니다.

$$ Z_{\text{Domain}} = \frac{\sum Z_k}{\frac{N}{\sqrt{N - (N-1)\alpha}}} = \frac{\sum Z_k \cdot \sqrt{N - (N-1)\alpha}}{N} $$

산술 평균 Z는 하위 척도들의 합에 개수(N)를 나눈 값입니다.

$$ \bar{Z} = \frac{\sum Z_k}{N} $$

최종적으로 상위 요인의 Z 점수는 다음과 같이 유도됩니다.

$$ Z_{\text{Domain}} = \bar{Z} \cdot \sqrt{N - (N-1)\alpha} $$

 

그런데 잘 생각해보니 알파 계수는 "각 하위 척도의 편차가 적을 수록 높아지고 각 하위 척도의 편차가 클 수록 낮아지는 경향"을 발견했습니다.

아까 위키백과에서 알파 계수 공식들 중에서 이런 공식이 있어요.

$$ \alpha = \frac{k}{k-1} \left( 1 - \frac{\sum \sigma_i^2}{\sigma_X^2} \right) $$

여기서 가장 핵심은 분모에 있는 전체 합산 점수의 분산입니다.

$$ \sigma_X^2 = \sum_{i=1}^k \sigma_i^2 + 2 \sum_{i < j} \text{Cov}(X_i, X_j) $$

이 식에서 하위 척도들의 Z점수가 고르게 일치할수록 척도 간의 시너지(공분산)가 극대화되어, 분모에 있는 전체 합산 점수의 분산이 이론적 최댓값으로 폭증하게 되며 alpha가 1에 가까워지게 됩니다. 반대로 하위 척도들의 점수 편차가 극단적이고 따로 놀 경우 시너지가 사라져, 전체 합산 점수의 분산이 개별 분산의 단순 합(k)으로 축소되면서 alpha가 0에 가까워지게 됩니다.

 

이것이 바로 고정된 시간에서 여러 항목들을 검사하는 자가 진단 테스트에서 알파 계수를 활용하여 Z 점수를 구할 수 있습니다.

이제 모든 공식 유도가 끝났으니 이제 Python으로 구현하겠습니다.

더보기
from decimal import Decimal, getcontext

# 부동소수점 오차 원천 차단을 위한 고정밀 연산 자릿수 설정 (28자리)
getcontext().prec = 28


class Big5SafeMathEngine:
    """하위 척도 분산 공식의 동적 알파와 규준 고정 알파 중

    안전 하한선 max(Dynamic, Fixed)를 적용하여 Z점수를 산출하는 최종 엔진
    """

    @staticmethod
    def calculate_rmsd_and_mean(facet_scores):
        """[공식 1] 산술 평균(Mean Z) 및 평균 제곱근 편차(RMSD) 연산"""
        scores = [Decimal(str(s)) for s in facet_scores]
        k = Decimal(len(scores))

        if k == 0:
            return Decimal("0"), Decimal("0"), k

        mean_z = sum(scores) / k
        sum_sq_dev = sum((s - mean_z) ** 2 for s in scores)
        rmsd = (sum_sq_dev / k).sqrt()

        return mean_z, rmsd, k

    @classmethod
    def calculate_dynamic_alpha(cls, facet_scores):
        """[공식 2] 분산 분할 법칙 기반 동적 크론바흐 알파 직접 연산"""
        mean_z, rmsd, k = cls.calculate_rmsd_and_mean(facet_scores)

        if k <= 1:
            return Decimal("1.0")

        sum_item_variance = k
        profile_var = rmsd**2
        sigma_x_squared = k + (k * (k - Decimal("1"))) / (
            Decimal("1") + profile_var * Decimal("4")
        )

        alpha = (k / (k - Decimal("1"))) * (
            Decimal("1") - (sum_item_variance / sigma_x_squared)
        )
        alpha = max(Decimal("0.0000"), min(Decimal("1.0000"), alpha))

        return alpha

    @classmethod
    def compile_safe_domain_score(cls, facet_scores, fixed_norm_alpha):
        """[공식 3 - 최종 보정] 안전 하한선 max(Dynamic, Fixed) 기반 Z점수 컴파일

        1. 계산된 동적 알파가 규준보다 높으면 -> 개인의 높은 일관성 그대로 존중
        2. 계산된 동적 알파가 규준보다 낮으면 -> 규준 고정값을 하한선 방파제로 적용
        """
        mean_z, rmsd, k = cls.calculate_rmsd_and_mean(facet_scores)

        if k == 0:
            return (
                Decimal("0"),
                Decimal("50"),
                Decimal("0"),
                Decimal("0"),
                Decimal("0"),
                "N/A",
            )

        # 동적 수학 알파 산출
        dynamic_alpha = cls.calculate_dynamic_alpha(facet_scores)
        fixed_alpha = Decimal(str(fixed_norm_alpha))

        # ★ 핵심 보정 아키텍처: 안전 하한선 max 선택
        if dynamic_alpha >= fixed_alpha:
            safe_alpha = dynamic_alpha
            selection_type = "동적 수학 알파 (고신뢰도 존중)"
        else:
            safe_alpha = fixed_alpha
            selection_type = "규준 고정 알파 (하한선 방어)"

        # 복합 표준편차 가중치 산출: k - (k - 1) * safe_alpha
        inner_weight = k - (k - Decimal("1")) * safe_alpha
        if inner_weight < Decimal("0"):
            inner_weight = Decimal("0")

        z_domain = mean_z * inner_weight.sqrt()
        t_domain = Decimal("50") + (z_domain * Decimal("10"))

        return z_domain, t_domain, safe_alpha, dynamic_alpha, rmsd, selection_type


# =====================================================================
# 📋 Big 5 (NEO-PI-R 30 Facets) 아키텍트님 조정 테스트 데이터셋
# =====================================================================
BIG5_30_FACETS_DATA = {
    "O 개방성": {
        "상상력(Imagination)": 1.75,
        "예술적 감수성(Artistic Interests)": 0.85,
        "감정 존중(Emotionality)": 2.50,
        "모험성(Adventurousness)": 1.95,
        "지적 호기심(Intellect)": 2.40,
        "가치 진보성(Liberalism)": 0.55,
    },
    "C 성실성": {
        "자신감(Self-efficacy)": 1.65,
        "계획성(Orderliness)": -0.45,
        "책임감(Dutifulness)": 1.35,
        "성취욕(Achievement-striving)": 2.25,
        "자제력(Self-discipline)": -0.05,
        "신중함(Cautiousness)": 1.25,
    },
    "E 외향성": {
        "친밀감(Warmth)": 1.75,
        "사교성(Gregariousness)": 1.55,
        "리더십(Assertiveness)": 1.85,
        "활동성(Activity)": 2.25,
        "흥미 추구(Excitement-seeking)": 2.75,
        "명랑함(Positive-emotions)": 2.15,
    },
    "A 우호성": {
        "신뢰(Trust)": 1.85,
        "정직함(Honesty)": 1.95,
        "이타주의(Altruism)": 2.40,
        "협조성(Cooperation)": 0.75,
        "겸손함(Modesty)": 0.85,
        "공감력(Sympathy)": 1.15,
    },
    "N 신경성": {
        "걱정(Anxiety)": -0.30,
        "분노(Anger)": 1.25,
        "우울(Depression)": 1.75,
        "자의식(Self-consciousness)": -0.35,
        "충동성(Impulsiveness)": 2.35,
        "심약함(Vulnerability)": 1.95,
    },
}

# 규준 기관의 고정 알파 (안전 하한선 기준치)
FIXED_NORM_ALPHAS = {
    "O 개방성": 0.7450,
    "C 성실성": 0.6500,
    "E 외향성": 0.7800,
    "A 우호성": 0.7000,
    "N 신경성": 0.7200,
}

if __name__ == "__main__":
    print("=" * 80)
    print(
        " [Big 5 NEO-PI-R 30 Facets 안전 하한선 max(Dynamic, Fixed) 최종 컴파일러]"
    )
    print("=" * 80)

    for domain_name, facets_dict in BIG5_30_FACETS_DATA.items():
        facet_scores = list(facets_dict.values())
        fixed_alpha = FIXED_NORM_ALPHAS[domain_name]

        z_dom, t_dom, safe_alpha, dyn_alpha, rmsd, sel_type = (
            Big5SafeMathEngine.compile_safe_domain_score(facet_scores, fixed_alpha)
        )

        mean_z = sum(facet_scores) / len(facet_scores)

        print(f"\n▶ {domain_name} (하위 척도 k={len(facet_scores)})")
        print("-" * 68)
        print(
            f"  • 산술 평균(Mean Z): {mean_z:+.4f} SD | 내부 편차(RMSD): {rmsd:.4f} SD"
        )
        print(
            f"  • 자체 계산 알파  : {dyn_alpha:.4f} vs 규준 고정 알파: {fixed_alpha:.4f}"
        )
        print(f"  ★ 최종 선택 알파  : {safe_alpha:.4f} ──> [{sel_type}]")
        print(
            f"  ★ 최종 상위 Z점수 : {z_dom:+.4f} SD (T점수 환산: {t_dom:.1f}점)"
        )
    print("\n" + "=" * 80)

 [결과]

================================================================================
 [Big 5 NEO-PI-R 30 Facets 안전 하한선 max(Dynamic, Fixed) 최종 컴파일러]
================================================================================

▶ O 개방성 (하위 척도 k=6)
--------------------------------------------------------------------
  • 산술 평균(Mean Z): +1.6667 SD | 내부 편차(RMSD): 0.7341 SD
  • 자체 계산 알파  : 0.7357 vs 규준 고정 알파: 0.7450
  ★ 최종 선택 알파  : 0.7450 ──> [규준 고정 알파 (하한선 방어)]
  ★ 최종 상위 Z점수 : +2.5139 SD (T점수 환산: 75.1점)

▶ C 성실성 (하위 척도 k=6)
--------------------------------------------------------------------
  • 산술 평균(Mean Z): +1.0000 SD | 내부 편차(RMSD): 0.9465 SD
  • 자체 계산 알파  : 0.6261 vs 규준 고정 알파: 0.6500
  ★ 최종 선택 알파  : 0.6500 ──> [규준 고정 알파 (하한선 방어)]
  ★ 최종 상위 Z점수 : +1.6583 SD (T점수 환산: 66.6점)

▶ E 외향성 (하위 척도 k=6)
--------------------------------------------------------------------
  • 산술 평균(Mean Z): +2.0500 SD | 내부 편차(RMSD): 0.3916 SD
  • 자체 계산 알파  : 0.9073 vs 규준 고정 알파: 0.7800
  ★ 최종 선택 알파  : 0.9073 ──> [동적 수학 알파 (고신뢰도 존중)]
  ★ 최종 상위 Z점수 : +2.4802 SD (T점수 환산: 74.8점)

▶ A 우호성 (하위 척도 k=6)
--------------------------------------------------------------------
  • 산술 평균(Mean Z): +1.4917 SD | 내부 편차(RMSD): 0.6113 SD
  • 자체 계산 알파  : 0.8006 vs 규준 고정 알파: 0.7000
  ★ 최종 선택 알파  : 0.8006 ──> [동적 수학 알파 (고신뢰도 존중)]
  ★ 최종 상위 Z점수 : +2.1080 SD (T점수 환산: 71.1점)

▶ N 신경성 (하위 척도 k=6)
--------------------------------------------------------------------
  • 산술 평균(Mean Z): +1.1083 SD | 내부 편차(RMSD): 1.0640 SD
  • 자체 계산 알파  : 0.5699 vs 규준 고정 알파: 0.7200
  ★ 최종 선택 알파  : 0.7200 ──> [규준 고정 알파 (하한선 방어)]
  ★ 최종 상위 Z점수 : +1.7170 SD (T점수 환산: 67.2점)

================================================================================

 

이렇게 해서 크론바흐 알파 계수 기반으로 상위 요인 Z점수를 계산하는 소스 코드를 구현했습니다.

 

이번에는 RMSD를 구하는 소스 코드를 구현하도록 하겠습니다.

RMSD(평균 제곱근 편차)는 아까 전의 크론바흐 알파 계수보다 더 쉬운 편입니다.

$$ RMSD = \sqrt{\frac{1}{N} \sum_{k=1}^{N} (Z_k)^2} $$

참고로 이미 정규화를 거쳤으므로 평균 0으로부터 얼마나 떨어져 있는지만 계산하면 됩니다.

이것을 소스 코드로 구현하면 다음과 같습니다.

def calculate_RMSD():
    print("=" * 80)
    print(" [Big 5 30 Facets 전체 극단성(RMSD) 단일 스칼라 산출 엔진]")
    print("=" * 80)

    # 1. 2차원 딕셔너리에서 30개의 모든 Z점수를 1차원 리스트로 평탄화(Flatten)
    all_scores = []
    for domain_name, facets_dict in BIG5_30_FACETS_DATA.items():
        all_scores.extend(facets_dict.values())

    # 2. 고정밀 연산을 위한 Decimal 변환 및 N 산출
    scores = [Decimal(str(s)) for s in all_scores]
    N = Decimal(len(scores))

    if N == 0:
        print("연산할 데이터가 존재하지 않습니다.")
        return Decimal("0")

    # 3. RMSD 수식 적용: root( sum(Z^2) / N )
    sum_sq = sum(s ** 2 for s in scores)
    rmsd_total = (sum_sq / N).sqrt()

    # 4. 결과 출력
    print(f"▶ 분석 대상 척도 수 (N)  : {int(N)}개")
    print(f"▶ Z점수 제곱의 합 (ΣZ²) : {sum_sq:.4f}")
    print(f"★ 30 척도 통합 RMSD 지수: {rmsd_total:.4f} SD")
    print("=" * 80)

    return rmsd_total

 

접은글 안에는 전체 소스 코드가 담겨져 있습니다.

더보기
from decimal import Decimal, getcontext

# 부동소수점 오차 원천 차단을 위한 고정밀 연산 자릿수 설정 (28자리)
getcontext().prec = 28


class Big5SafeMathEngine:
    """하위 척도 분산 공식의 동적 알파와 규준 고정 알파 중

    안전 하한선 max(Dynamic, Fixed)를 적용하여 Z점수를 산출하는 최종 엔진
    """

    @staticmethod
    def calculate_rmsd_and_mean(facet_scores):
        """[공식 1] 산술 평균(Mean Z) 및 평균 제곱근 편차(RMSD) 연산"""
        scores = [Decimal(str(s)) for s in facet_scores]
        k = Decimal(len(scores))

        if k == 0:
            return Decimal("0"), Decimal("0"), k

        mean_z = sum(scores) / k
        sum_sq_dev = sum((s - mean_z) ** 2 for s in scores)
        rmsd = (sum_sq_dev / k).sqrt()

        return mean_z, rmsd, k

    @classmethod
    def calculate_dynamic_alpha(cls, facet_scores):
        """[공식 2] 분산 분할 법칙 기반 동적 크론바흐 알파 직접 연산"""
        mean_z, rmsd, k = cls.calculate_rmsd_and_mean(facet_scores)

        if k <= 1:
            return Decimal("1.0")

        sum_item_variance = k
        profile_var = rmsd**2
        sigma_x_squared = k + (k * (k - Decimal("1"))) / (
            Decimal("1") + profile_var * Decimal("4")
        )

        alpha = (k / (k - Decimal("1"))) * (
            Decimal("1") - (sum_item_variance / sigma_x_squared)
        )
        alpha = max(Decimal("0.0000"), min(Decimal("1.0000"), alpha))

        return alpha

    @classmethod
    def compile_safe_domain_score(cls, facet_scores, fixed_norm_alpha):
        """[공식 3 - 최종 보정] 안전 하한선 max(Dynamic, Fixed) 기반 Z점수 컴파일

        1. 계산된 동적 알파가 규준보다 높으면 -> 개인의 높은 일관성 그대로 존중
        2. 계산된 동적 알파가 규준보다 낮으면 -> 규준 고정값을 하한선 방파제로 적용
        """
        mean_z, rmsd, k = cls.calculate_rmsd_and_mean(facet_scores)

        if k == 0:
            return (
                Decimal("0"),
                Decimal("50"),
                Decimal("0"),
                Decimal("0"),
                Decimal("0"),
                "N/A",
            )

        # 동적 수학 알파 산출
        dynamic_alpha = cls.calculate_dynamic_alpha(facet_scores)
        fixed_alpha = Decimal(str(fixed_norm_alpha))

        # ★ 핵심 보정 아키텍처: 안전 하한선 max 선택
        if dynamic_alpha >= fixed_alpha:
            safe_alpha = dynamic_alpha
            selection_type = "동적 수학 알파 (고신뢰도 존중)"
        else:
            safe_alpha = fixed_alpha
            selection_type = "규준 고정 알파 (하한선 방어)"

        # 복합 표준편차 가중치 산출: k - (k - 1) * safe_alpha
        inner_weight = k - (k - Decimal("1")) * safe_alpha
        if inner_weight < Decimal("0"):
            inner_weight = Decimal("0")

        z_domain = mean_z * inner_weight.sqrt()
        t_domain = Decimal("50") + (z_domain * Decimal("10"))

        return z_domain, t_domain, safe_alpha, dynamic_alpha, rmsd, selection_type
    
def calculate_Z_domain_values() :
    print("=" * 80)
    print(
        " [Big 5 NEO-PI-R 30 Facets 안전 하한선 max(Dynamic, Fixed) 최종 컴파일러]"
    )
    print("=" * 80)

    for domain_name, facets_dict in BIG5_30_FACETS_DATA.items():
        facet_scores = list(facets_dict.values())
        fixed_alpha = FIXED_NORM_ALPHAS[domain_name]

        z_dom, t_dom, safe_alpha, dyn_alpha, rmsd, sel_type = (
            Big5SafeMathEngine.compile_safe_domain_score(facet_scores, fixed_alpha)
        )

        mean_z = sum(facet_scores) / len(facet_scores)

        print(f"\n▶ {domain_name} (하위 척도 k={len(facet_scores)})")
        print("-" * 68)
        print(
            f"  • 산술 평균(Mean Z): {mean_z:+.4f} SD | 내부 편차(RMSD): {rmsd:.4f} SD"
        )
        print(
            f"  • 자체 계산 알파  : {dyn_alpha:.4f} vs 규준 고정 알파: {fixed_alpha:.4f}"
        )
        print(f"  ★ 최종 선택 알파  : {safe_alpha:.4f} ──> [{sel_type}]")
        print(
            f"  ★ 최종 상위 Z점수 : {z_dom:+.4f} SD (T점수 환산: {t_dom:.1f}점)"
        )
    print("\n" + "=" * 80 + "\n")
    
def calculate_RMSD():
    print("\n" + "=" * 80)
    print(" [Big 5 30 Facets 전체 극단성(RMSD) 단일 스칼라 산출 엔진]")
    print("=" * 80)

    # 1. 2차원 딕셔너리에서 30개의 모든 Z점수를 1차원 리스트로 평탄화(Flatten)
    all_scores = []
    for domain_name, facets_dict in BIG5_30_FACETS_DATA.items():
        all_scores.extend(facets_dict.values())

    # 2. 고정밀 연산을 위한 Decimal 변환 및 N 산출
    scores = [Decimal(str(s)) for s in all_scores]
    N = Decimal(len(scores))

    if N == 0:
        print("연산할 데이터가 존재하지 않습니다.")
        return Decimal("0")

    # 3. RMSD 수식 적용: root( sum(Z^2) / N )
    sum_sq = sum(s ** 2 for s in scores)
    rmsd_total = (sum_sq / N).sqrt()

    # 4. 결과 출력
    print(f"▶ 분석 대상 척도 수 (N)  : {int(N)}개")
    print(f"▶ Z점수 제곱의 합 (ΣZ²) : {sum_sq:.4f}")
    print(f"★ 30 척도 통합 RMSD 지수: {rmsd_total:.4f} SD")
    print("=" * 80)

    return rmsd_total

# =====================================================================
# 📋 Big 5 (NEO-PI-R 30 Facets) 테스트 데이터셋
# =====================================================================
BIG5_30_FACETS_DATA = {
    "O 개방성": {
        "상상력(Imagination)": 1.75,
        "예술적 감수성(Artistic Interests)": 0.85,
        "감정 존중(Emotionality)": 2.50,
        "모험성(Adventurousness)": 1.95,
        "지적 호기심(Intellect)": 2.40,
        "가치 진보성(Liberalism)": 0.55,
    },
    "C 성실성": {
        "자신감(Self-efficacy)": 1.65,
        "계획성(Orderliness)": -0.45,
        "책임감(Dutifulness)": 1.35,
        "성취욕(Achievement-striving)": 2.25,
        "자제력(Self-discipline)": -0.05,
        "신중함(Cautiousness)": 1.25,
    },
    "E 외향성": {
        "친밀감(Warmth)": 1.75,
        "사교성(Gregariousness)": 1.55,
        "리더십(Assertiveness)": 1.85,
        "활동성(Activity)": 2.25,
        "흥미 추구(Excitement-seeking)": 2.75,
        "명랑함(Positive-emotions)": 2.15,
    },
    "A 우호성": {
        "신뢰(Trust)": 1.85,
        "정직함(Honesty)": 1.95,
        "이타주의(Altruism)": 2.40,
        "협조성(Cooperation)": 0.75,
        "겸손함(Modesty)": 0.85,
        "공감력(Sympathy)": 1.15,
    },
    "N 신경성": {
        "걱정(Anxiety)": -0.30,
        "분노(Anger)": 1.25,
        "우울(Depression)": 1.75,
        "자의식(Self-consciousness)": -0.35,
        "충동성(Impulsiveness)": 2.35,
        "심약함(Vulnerability)": 1.95,
    },
}

# 규준 기관의 고정 알파 (안전 하한선 기준치)
FIXED_NORM_ALPHAS = {
    "O 개방성": 0.7450,
    "C 성실성": 0.6500,
    "E 외향성": 0.7800,
    "A 우호성": 0.7000,
    "N 신경성": 0.7200,
}

if __name__ == "__main__":
    calculate_Z_domain_values()
    calculate_RMSD()

 

[결과]

================================================================================
 [Big 5 NEO-PI-R 30 Facets 안전 하한선 max(Dynamic, Fixed) 최종 컴파일러]
================================================================================

▶ O 개방성 (하위 척도 k=6)
--------------------------------------------------------------------
  • 산술 평균(Mean Z): +1.6667 SD | 내부 편차(RMSD): 0.7341 SD
  • 자체 계산 알파  : 0.7357 vs 규준 고정 알파: 0.7450
  ★ 최종 선택 알파  : 0.7450 ──> [규준 고정 알파 (하한선 방어)]
  ★ 최종 상위 Z점수 : +2.5139 SD (T점수 환산: 75.1점)

▶ C 성실성 (하위 척도 k=6)
--------------------------------------------------------------------
  • 산술 평균(Mean Z): +1.0000 SD | 내부 편차(RMSD): 0.9465 SD
  • 자체 계산 알파  : 0.6261 vs 규준 고정 알파: 0.6500
  ★ 최종 선택 알파  : 0.6500 ──> [규준 고정 알파 (하한선 방어)]
  ★ 최종 상위 Z점수 : +1.6583 SD (T점수 환산: 66.6점)

▶ E 외향성 (하위 척도 k=6)
--------------------------------------------------------------------
  • 산술 평균(Mean Z): +2.0500 SD | 내부 편차(RMSD): 0.3916 SD
  • 자체 계산 알파  : 0.9073 vs 규준 고정 알파: 0.7800
  ★ 최종 선택 알파  : 0.9073 ──> [동적 수학 알파 (고신뢰도 존중)]
  ★ 최종 상위 Z점수 : +2.4802 SD (T점수 환산: 74.8점)

▶ A 우호성 (하위 척도 k=6)
--------------------------------------------------------------------
  • 산술 평균(Mean Z): +1.4917 SD | 내부 편차(RMSD): 0.6113 SD
  • 자체 계산 알파  : 0.8006 vs 규준 고정 알파: 0.7000
  ★ 최종 선택 알파  : 0.8006 ──> [동적 수학 알파 (고신뢰도 존중)]
  ★ 최종 상위 Z점수 : +2.1080 SD (T점수 환산: 71.1점)

▶ N 신경성 (하위 척도 k=6)
--------------------------------------------------------------------
  • 산술 평균(Mean Z): +1.1083 SD | 내부 편차(RMSD): 1.0640 SD
  • 자체 계산 알파  : 0.5699 vs 규준 고정 알파: 0.7200
  ★ 최종 선택 알파  : 0.7200 ──> [규준 고정 알파 (하한선 방어)]
  ★ 최종 상위 Z점수 : +1.7170 SD (T점수 환산: 67.2점)

================================================================================


================================================================================
 [Big 5 30 Facets 전체 극단성(RMSD) 단일 스칼라 산출 엔진]
================================================================================
▶ 분석 대상 척도 수 (N)  : 30개
▶ Z점수 제곱의 합 (ΣZ²) : 87.1650
★ 30 척도 통합 RMSD 지수: 1.7046 SD
================================================================================

 

 

그런데 문득 이런 생각이 들었습니다.

"이거 아예 프로젝트 형식으로 만들어서 데이터 수집하면 될 것 같은데?"

그래서 저는 다음과 같이 아예 프로젝트 폴더를 만들었습니다.

[sample.py]

더보기
# =====================================================================
# 📋 Big 5 (NEO-PI-R 30 Facets) 조정 테스트 데이터셋
# =====================================================================
BIG5_30_FACETS_DATA = {
    "O 개방성": {
        "상상력(Imagination)": 1.75,
        "예술적 감수성(Artistic Interests)": 0.85,
        "감정 존중(Emotionality)": 2.50,
        "모험성(Adventurousness)": 1.95,
        "지적 호기심(Intellect)": 2.40,
        "가치 진보성(Liberalism)": 0.55,
    },
    "C 성실성": {
        "자신감(Self-efficacy)": 1.65,
        "계획성(Orderliness)": -0.45,
        "책임감(Dutifulness)": 1.35,
        "성취욕(Achievement-striving)": 2.25,
        "자제력(Self-discipline)": -0.05,
        "신중함(Cautiousness)": 1.25,
    },
    "E 외향성": {
        "친밀감(Warmth)": 1.75,
        "사교성(Gregariousness)": 1.55,
        "리더십(Assertiveness)": 1.85,
        "활동성(Activity)": 2.25,
        "흥미 추구(Excitement-seeking)": 2.75,
        "명랑함(Positive-emotions)": 2.15,
    },
    "A 우호성": {
        "신뢰(Trust)": 1.85,
        "정직함(Honesty)": 1.95,
        "이타주의(Altruism)": 2.40,
        "협조성(Cooperation)": 0.75,
        "겸손함(Modesty)": 0.85,
        "공감력(Sympathy)": 1.15,
    },
    "N 신경성": {
        "걱정(Anxiety)": -0.30,
        "분노(Anger)": 1.25,
        "우울(Depression)": 1.75,
        "자의식(Self-consciousness)": -0.35,
        "충동성(Impulsiveness)": 2.35,
        "심약함(Vulnerability)": 1.95,
    },
}

# 규준 기관의 고정 알파 (안전 하한선 기준치)
FIXED_NORM_ALPHAS = {
    "O 개방성": 0.7450,
    "C 성실성": 0.6500,
    "E 외향성": 0.7800,
    "A 우호성": 0.7000,
    "N 신경성": 0.7200,
}

 

[calculate.py]

더보기
from decimal import Decimal, getcontext

# 부동소수점 오차 원천 차단을 위한 고정밀 연산 자릿수 설정 (28자리)
getcontext().prec = 28

class Big5SafeMathEngine:
    """하위 척도 분산 공식의 동적 알파와 규준 고정 알파 중

    안전 하한선 max(Dynamic, Fixed)를 적용하여 Z점수를 산출하는 최종 엔진
    """

    @staticmethod
    def calculate_rmsd_and_mean(facet_scores):
        """[공식 1] 산술 평균(Mean Z) 및 평균 제곱근 편차(RMSD) 연산"""
        scores = [Decimal(str(s)) for s in facet_scores]
        k = Decimal(len(scores))

        if k == 0:
            return Decimal("0"), Decimal("0"), k

        mean_z = sum(scores) / k
        sum_sq_dev = sum((s - mean_z) ** 2 for s in scores)
        rmsd = (sum_sq_dev / k).sqrt()

        return mean_z, rmsd, k

    @classmethod
    def calculate_dynamic_alpha(cls, facet_scores):
        """[공식 2] 분산 분할 법칙 기반 동적 크론바흐 알파 직접 연산"""
        mean_z, rmsd, k = cls.calculate_rmsd_and_mean(facet_scores)

        if k <= 1:
            return Decimal("1.0")

        sum_item_variance = k
        profile_var = rmsd**2
        sigma_x_squared = k + (k * (k - Decimal("1"))) / (
            Decimal("1") + profile_var * Decimal("4")
        )

        alpha = (k / (k - Decimal("1"))) * (
            Decimal("1") - (sum_item_variance / sigma_x_squared)
        )
        alpha = max(Decimal("0.0000"), min(Decimal("1.0000"), alpha))

        return alpha

    @classmethod
    def compile_safe_domain_score(cls, facet_scores, fixed_norm_alpha):
        """[공식 3 - 최종 보정] 안전 하한선 max(Dynamic, Fixed) 기반 Z점수 컴파일

        1. 계산된 동적 알파가 규준보다 높으면 -> 개인의 높은 일관성 그대로 존중
        2. 계산된 동적 알파가 규준보다 낮으면 -> 규준 고정값을 하한선 방파제로 적용
        """
        mean_z, rmsd, k = cls.calculate_rmsd_and_mean(facet_scores)

        if k == 0:
            return (
                Decimal("0"),
                Decimal("50"),
                Decimal("0"),
                Decimal("0"),
                Decimal("0"),
                "N/A",
            )

        # 동적 수학 알파 산출
        dynamic_alpha = cls.calculate_dynamic_alpha(facet_scores)
        fixed_alpha = Decimal(str(fixed_norm_alpha))

        # ★ 핵심 보정 아키텍처: 안전 하한선 max 선택
        if dynamic_alpha >= fixed_alpha:
            safe_alpha = dynamic_alpha
            selection_type = "동적 수학 알파 (고신뢰도 존중)"
        else:
            safe_alpha = fixed_alpha
            selection_type = "규준 고정 알파 (하한선 방어)"

        # 복합 표준편차 가중치 산출: k - (k - 1) * safe_alpha
        inner_weight = k - (k - Decimal("1")) * safe_alpha
        if inner_weight < Decimal("0"):
            inner_weight = Decimal("0")

        z_domain = mean_z * inner_weight.sqrt()
        t_domain = Decimal("50") + (z_domain * Decimal("10"))

        return z_domain, t_domain, safe_alpha, dynamic_alpha, rmsd, selection_type
    
def calculate_Z_domain_values(BIG5_30_FACETS_DATA:dict, FIXED_NORM_ALPHAS:dict) :
    print("=" * 80)
    print(
        " [Big 5 NEO-PI-R 30 Facets 안전 하한선 max(Dynamic, Fixed) 최종 컴파일러]"
    )
    print("=" * 80)

    for domain_name, facets_dict in BIG5_30_FACETS_DATA.items():
        facet_scores = list(facets_dict.values())
        fixed_alpha = FIXED_NORM_ALPHAS[domain_name]

        z_dom, t_dom, safe_alpha, dyn_alpha, rmsd, sel_type = (
            Big5SafeMathEngine.compile_safe_domain_score(facet_scores, fixed_alpha)
        )

        mean_z = sum(facet_scores) / len(facet_scores)

        print(f"\n▶ {domain_name} (하위 척도 k={len(facet_scores)})")
        print("-" * 68)
        print(
            f"  • 산술 평균(Mean Z): {mean_z:+.4f} SD | 내부 편차(RMSD): {rmsd:.4f} SD"
        )
        print(
            f"  • 자체 계산 알파  : {dyn_alpha:.4f} vs 규준 고정 알파: {fixed_alpha:.4f}"
        )
        print(f"  ★ 최종 선택 알파  : {safe_alpha:.4f} ──> [{sel_type}]")
        print(
            f"  ★ 최종 상위 Z점수 : {z_dom:+.4f} SD (T점수 환산: {t_dom:.1f}점)"
        )
    print("\n" + "=" * 80)
    
def calculate_RMSD(BIG5_30_FACETS_DATA:dict):
    print("=" * 80)
    print(" [Big 5 30 Facets 전체 극단성(RMSD) 단일 스칼라 산출 엔진]")
    print("=" * 80)

    # 1. 2차원 딕셔너리에서 30개의 모든 Z점수를 1차원 리스트로 평탄화(Flatten)
    all_scores = []
    for domain_name, facets_dict in BIG5_30_FACETS_DATA.items():
        all_scores.extend(facets_dict.values())

    # 2. 고정밀 연산을 위한 Decimal 변환 및 N 산출
    scores = [Decimal(str(s)) for s in all_scores]
    N = Decimal(len(scores))

    if N == 0:
        print("연산할 데이터가 존재하지 않습니다.")
        return Decimal("0")

    # 3. RMSD 수식 적용: root( sum(Z^2) / N )
    sum_sq = sum(s ** 2 for s in scores)
    rmsd_total = (sum_sq / N).sqrt()

    # 4. 결과 출력
    print(f"▶ 분석 대상 척도 수 (N)  : {int(N)}개")
    print(f"▶ Z점수 제곱의 합 (ΣZ²) : {sum_sq:.4f}")
    print(f"★ 30 척도 통합 RMSD 지수: {rmsd_total:.4f} SD")
    print("=" * 80)

    return rmsd_total

 

[main.py]

더보기
# 1. 외부 모듈에서 샘플 데이터(상수) 적재
from sample import BIG5_30_FACETS_DATA, FIXED_NORM_ALPHAS

# 2. 외부 모듈에서 핵심 연산 엔진(함수) 적재
from calculate import calculate_Z_domain_values, calculate_RMSD

# Java에서 main(String args[]) 역할과 동일한 함수입니다. 여기가 실행부입니다.
def main():
    # 파이프라인 1 : 5대 도메인 상위 Z점수 컴파일 및 출력
    calculate_Z_domain_values(BIG5_30_FACETS_DATA, FIXED_NORM_ALPHAS)
    
    # 두 출력 결과 사이의 시각적 간격을 맞추기 위한 빈 줄 출력
    print("\n")
    
    # 파이프라인 2 : 30개 하위 척도 통합 RMSD 컴파일 및 출력
    calculate_RMSD(BIG5_30_FACETS_DATA)

if __name__ == "__main__":
    main()

 

 

이렇게 해서 main.py에서 실행(run)을 하면 동일한 분석 결과를 얻을 수 있습니다.

이제 data_loader.py로 엑셀 안에 있는 데이터들을 읽고 싶습니다.

우선 프로젝트 폴더 안에 엑셀 파일 2개를 추가합니다.

Big5_DB.xlsx : 고유 번호(ID), 이름, 성별, 생년월일, MBTI, 에니어그램 유형, 트라이타입, 30개의 하위 척도 Z점수가 기록되어 있습니다.

Big5_alpha.xlsx : 각 상위 요인의 알파 계수가 기록되어 있습니다.

Big5_alpha.xlsx
0.01MB
Big5_DB.xlsx
0.01MB

지금부터 이 자료들을 바탕으로 data_loader.py 소스 코드를 작성하도록 하겠습니다.

 

그 이전에 엑셀 파일은 외부 라이브러리 pandas, openpyxl가 필요합니다.

그래서 VS Code 터미널에 "pip install pandas", "pip install openpyxl" 명령어 입력하고 외부 라이브러리 pandas, openpyxl 라이브러리를 설치해야 합니다.

이 때 되도록 별도의 가상 환경도 설정해주시길 바랍니다.

만약 가상 환경(.venv) 만들었다면 ".\.venv\Scripts\Activate.ps1" 해주고 그 다음에 외부 라이브러리들을 설치해주세요.

원래의 전역 환경으로 돌아갈려면 "deactivate" 하나 입력해주세요.

 

이제부터 pandas와 openpyxl로 직접 엑셀 파일들을 읽어내서 Python에 데이터들을 불러오는 data_loader.py는 다음과 같습니다.

더보기
import pandas as pd
import json

def load_big5_databases(db_path="Big5_DB.xlsx", alpha_path="Big5_alpha.xlsx"):
    """
    엑셀 파일들을 읽어들여 시스템에서 사용할 수 있는 Dict(JSON) 형태로 반환하는 로더 함수
    """
    
    # =====================================================================
    # 1. FIXED_NORM_ALPHAS_DB 추출 파이프라인
    # =====================================================================
    df_alpha = pd.read_excel(alpha_path)
    
    # 엑셀의 단순 영문 헤더를 시스템 규격 이름으로 매핑
    domain_mapping = {
        "O": "O 개방성",
        "C": "C 성실성",
        "E": "E 외향성",
        "A": "A 우호성",
        "N": "N 신경성"
    }
    
    # DB의 영문 헤더를 calculate.py가 요구하는 원본 규격으로 복원하는 매퍼 추가
    facet_mapping = {
        "Imagination": "상상력(Imagination)", "Artistic Interests": "예술적 감수성(Artistic Interests)", 
        "Emotionality": "감정 존중(Emotionality)", "Adventurousness": "모험성(Adventurousness)", 
        "Intellect": "지적 호기심(Intellect)", "Liberalism": "가치 진보성(Liberalism)",
        
        "Self-efficacy": "자신감(Self-efficacy)", "Orderliness": "계획성(Orderliness)", 
        "Dutifulness": "책임감(Dutifulness)", "Achievement-striving": "성취욕(Achievement-striving)", 
        "Self-discipline": "자제력(Self-discipline)", "Cautiousness": "신중함(Cautiousness)",
        
        "Warmth": "친밀감(Warmth)", "Gregariousness": "사교성(Gregariousness)", 
        "Assertiveness": "리더십(Assertiveness)", "Activity": "활동성(Activity)", 
        "Excitement-seeking": "흥미 추구(Excitement-seeking)", "Positive-emotions": "명랑함(Positive-emotions)",
        
        "Trust": "신뢰(Trust)", "Honesty": "정직함(Honesty)", 
        "Altruism": "이타주의(Altruism)", "Cooperation": "협조성(Cooperation)", 
        "Modesty": "겸손함(Modesty)", "Sympathy": "공감력(Sympathy)",
        
        "Anxiety": "걱정(Anxiety)", "Anger": "분노(Anger)", 
        "Depression": "우울(Depression)", "Self-consciousness": "자의식(Self-consciousness)", 
        "Impulsiveness": "충동성(Impulsiveness)", "Vulnerability": "심약함(Vulnerability)"
    }
    
    FIXED_NORM_ALPHAS_DB = {}
    for col in df_alpha.columns:
        if col in domain_mapping:
            # 첫 번째 행의 값을 float으로 안전하게 변환하여 저장
            FIXED_NORM_ALPHAS_DB[domain_mapping[col]] = float(df_alpha[col].iloc[0])

    # =====================================================================
    # 2. BIG5_DB_DATA 추출 파이프라인
    # =====================================================================
    # header=None으로 읽어와서 0행(대분류), 1행(소분류)을 직접 컨트롤합니다.
    df_db = pd.read_excel(db_path, header=None)
    
    # 하위 척도(Facet)가 위치한 1행의 값들을 리스트로 추출
    facets_row = df_db.iloc[1].fillna("").astype(str).tolist()
    
    # 5대 도메인이 위치한 Column 인덱스 범위 하드코딩 (아키텍처 구조 고정)
    domain_ranges = {
        "O 개방성": range(7, 13),
        "C 성실성": range(13, 19),
        "E 외향성": range(19, 25),
        "A 우호성": range(25, 31),
        "N 신경성": range(31, 37)
    }
    
    BIG5_DB_DATA = {}
    
    # 결측치(NaN)를 파이썬의 None(JSON의 null)으로 변환해 주는 헬퍼 함수
    def get_value(v):
        if pd.isna(v): 
            return None
        return v
    
    # 실제 데이터가 시작되는 2행(Index 2)부터 끝까지 순회
    for index in range(2, len(df_db)):
        row = df_db.iloc[index]
        user_id = str(row[0])
        
        # ID가 없거나 비어있는 빈 행은 건너뜀 (안전 장치)
        if pd.isna(row[0]) or user_id.strip() == "":
            continue
            
        # 개별 유저 데이터 딕셔너 조립
        user_data = {
            "name": get_value(row[1]),
            "gender": get_value(row[2]),
            "birth date": get_value(row[3]),
            "MBTI": get_value(row[4]),
            "Enneagram": get_value(row[5]),
            "Tritype": get_value(row[6]),
            "BIG5_30_FACETS_DATA": {}
        }
        
        # Z-score 30개 파싱 및 도메인별 딕셔너리 할당
        for domain, col_range in domain_ranges.items():
            user_data["BIG5_30_FACETS_DATA"][domain] = {}
            for col_index in col_range:
                # 1. 원본 영문 헤더 추출 후 공백 제거
                eng_facet_name = facets_row[col_index].strip()
                
                # 2. 매퍼를 통해 원본 한글명으로 변환 (없을 경우 영문명 그대로 사용)
                full_facet_name = facet_mapping.get(eng_facet_name, eng_facet_name)
                
                value = get_value(row[col_index])
                
                # 3. 변환된 full_facet_name(한글+괄호)을 Key로 사용하여 할당
                user_data["BIG5_30_FACETS_DATA"][domain][full_facet_name] = float(value) if value is not None else 0.0
                
        # 고유 ID를 Key로 하여 마스터 딕셔너리에 추가
        BIG5_DB_DATA[user_id] = user_data
        
    return BIG5_DB_DATA, FIXED_NORM_ALPHAS_DB

# =====================================================================
# 테스트 실행 (직접 실행될 때만 작동)
# =====================================================================
if __name__ == "__main__":
    db_data, alpha_data = load_big5_databases()
    
    print("=" * 60)
    print(" [FIXED_NORM_ALPHAS_DB 로드 완료]")
    print("=" * 60)
    print(json.dumps(alpha_data, indent=4, ensure_ascii=False))
    
    print("\n" + "=" * 60)
    print(" [BIG5_DB_DATA 로드 완료 (첫 번째 유저 샘플)]")
    print("=" * 60)
    
    # 전체를 출력하면 너무 길어지므로, 첫 번째 유저(USER_001)만 예시로 출력
    first_user_id = list(db_data.keys())[0]
    first_user_data = {first_user_id: db_data[first_user_id]}
    print(json.dumps(first_user_data, indent=4, ensure_ascii=False))

[테스트 결과]

============================================================
 [FIXED_NORM_ALPHAS_DB 로드 완료]
============================================================
{
    "O 개방성": 0.745,
    "C 성실성": 0.805,
    "E 외향성": 0.755,
    "A 우호성": 0.775,
    "N 신경성": 0.815
}

============================================================
 [BIG5_DB_DATA 로드 완료 (첫 번째 유저 샘플)]
============================================================
{
    "USER_001": {
        "name": "Logician",
        "gender": "Female",
        "birth date": null,
        "MBTI": "INTP",
        "Enneagram": "5w4",
        "Tritype": 594,
        "BIG5_30_FACETS_DATA": {
            "O 개방성": {
                "상상력(Imagination)": 2.15,
                "예술적 감수성(Artistic Interests)": 0.46,
                "감정 존중(Emotionality)": -1.31,
                "모험성(Adventurousness)": 0.96,
                "지적 호기심(Intellect)": 2.13,
                "가치 진보성(Liberalism)": 0.93
            },
            "C 성실성": {
                "자신감(Self-efficacy)": 1.47,
                "계획성(Orderliness)": -1.27,
                "책임감(Dutifulness)": -0.64,
                "성취욕(Achievement-striving)": 0.66,
                "자제력(Self-discipline)": -1.64,
                "신중함(Cautiousness)": 0.36
            },
            "E 외향성": {
                "친밀감(Warmth)": -0.93,
                "사교성(Gregariousness)": -2.07,
                "리더십(Assertiveness)": -1.02,
                "활동성(Activity)": -0.67,
                "흥미 추구(Excitement-seeking)": -0.3,
                "명랑함(Positive-emotions)": -0.41
            },
            "A 우호성": {
                "신뢰(Trust)": -0.77,
                "정직함(Honesty)": 0.58,
                "이타주의(Altruism)": -0.56,
                "협조성(Cooperation)": -1.07,
                "겸손함(Modesty)": 0.02,
                "공감력(Sympathy)": -1.93
            },
            "N 신경성": {
                "걱정(Anxiety)": -0.16,
                "분노(Anger)": -0.97,
                "우울(Depression)": 1.45,
                "자의식(Self-consciousness)": 1.61,
                "충동성(Impulsiveness)": -1.18,
                "심약함(Vulnerability)": 1.41
            }
        }
    }
}

 

만약 가상 환경을 잘 설정하고 pandas, openpyxl 라이브러리를 잘 설치했다면 테스트 결과가 잘 나올 것입니다.

 

마지막으로 main 함수에서 직접 소스 코드를 작성해서 sample.py 사용하지 말고 data_loader.py로 데이터들을 추출해서 정말로 다양한 통계를 낼 수 있습니다.

[main.py]

더보기
# 1. 외부 모듈에서 샘플 데이터(상수) 적재
from sample import BIG5_30_FACETS_DATA, FIXED_NORM_ALPHAS

# 2. 외부 모듈에서 데이터 로더 적재
from data_loader import load_big5_databases

# 3. 외부 모듈에서 핵심 연산 엔진(함수) 적재
from calculate import Big5SafeMathEngine, calculate_RMSD, calculate_Z_domain_values

def calculate_single_user_rmsd(facet_scores_dict):
    """특정 유저의 30개 하위 척도 딕셔너리를 받아 RMSD를 반환하는 헬퍼 함수"""
    all_scores = []
    for facets in facet_scores_dict.values():
        all_scores.extend(facets.values())
    
    # 2차원 리스트 평탄화 후 내부 함수인 calculate_rmsd_and_mean 재활용
    # calculate_rmsd_and_mean은 (mean_z, rmsd, k) 튜플을 반환함
    _, rmsd, _ = Big5SafeMathEngine.calculate_rmsd_and_mean(all_scores)
    return float(rmsd)

def sample_data_loading_version() :
    # 파이프라인 1 : 5대 도메인 상위 Z점수 컴파일 및 출력
    calculate_Z_domain_values(BIG5_30_FACETS_DATA, FIXED_NORM_ALPHAS)
    
    # 두 출력 결과 사이의 시각적 간격을 맞추기 위한 빈 줄 출력
    print("\n")
    
    # 파이프라인 2 : 30개 하위 척도 통합 RMSD 컴파일 및 출력
    calculate_RMSD(BIG5_30_FACETS_DATA)

def excel_data_loading_version() :
    # 1. 데이터베이스 로드
    BIG5_DB_DATA, FIXED_NORM_ALPHAS_DB = load_big5_databases("Big5_DB.xlsx", "Big5_alpha.xlsx")
    
    # 결과를 담을 랭킹 리스트 선언
    # 딕셔너리 구조: {"ID": "...", "MBTI": "...", "RMSD": 1.5, "O": 2.1, "C": -1.2, ...}
    ranking_data = []

    # 2. 유저 순회하며 통계 데이터 추출
    for user_id, user_info in BIG5_DB_DATA.items():
        mbti = user_info["MBTI"]
        facets_data = user_info["BIG5_30_FACETS_DATA"]
        
        user_result = {
            "ID": user_id,
            "MBTI": mbti,
        }
        
        # 2-1. 도메인별 Z-score 연산 및 저장
        for domain_name, facets_dict in facets_data.items():
            facet_scores = list(facets_dict.values())
            fixed_alpha = FIXED_NORM_ALPHAS_DB[domain_name]
            
            # z_domain은 튜플의 첫 번째[0] 요소
            z_dom, _, _, _, _, _ = Big5SafeMathEngine.compile_safe_domain_score(facet_scores, fixed_alpha)
            
            # "O 개방성" -> "O" 형태로 키 값 축약
            domain_key = domain_name[0] 
            user_result[domain_key] = float(z_dom)
            
        # 2-2. 통합 RMSD 연산 및 저장
        user_result["RMSD"] = calculate_single_user_rmsd(facets_data)
        
        ranking_data.append(user_result)

    # 3. 순위표 출력 헬퍼 함수 (내림차순 정렬)
    def print_ranking(metric, title):
        # metric(예: "RMSD", "O")을 기준으로 내림차순 정렬
        sorted_data = sorted(ranking_data, key=lambda x: x[metric], reverse=True)
        
        print(f"\n{'='*50}")
        print(f" [🏆 {title} 순위표 (Top to Bottom)]")
        print(f"{'='*50}")
        for rank, data in enumerate(sorted_data, 1):
            score = data[metric]
            print(f"{rank:2d}위 | {data['MBTI']:<4} | {score:+.4f} SD")

    # 4. 최종 결과 출력 파이프라인 가동
    print_ranking("RMSD", "통합 극단성 (RMSD)")
    print_ranking("O", "O 개방성 (Openness)")
    print_ranking("C", "C 성실성 (Conscientiousness)")
    print_ranking("E", "E 외향성 (Extraversion)")
    print_ranking("A", "A 우호성 (Agreeableness)")
    print_ranking("N", "N 신경성 (Neuroticism)")
    print("\n" + "="*50)

def main():
    # sample_data_loading_version()
    excel_data_loading_version()

if __name__ == "__main__":
    main()

[테스트 결과]

==================================================
 [🏆 통합 극단성 (RMSD) 순위표 (Top to Bottom)]
==================================================
 1위 | ESTP | +1.4404 SD
 2위 | ESFP | +1.3827 SD
 3위 | ENTP | +1.3704 SD
 4위 | ENTJ | +1.3384 SD
 5위 | INTJ | +1.3197 SD
 6위 | ESTJ | +1.2939 SD
 7위 | ISTJ | +1.2104 SD
 8위 | ENFP | +1.1901 SD
 9위 | INTP | +1.1849 SD
10위 | INFP | +1.1380 SD
11위 | ISFJ | +1.1293 SD
12위 | INFJ | +1.1245 SD
13위 | ESFJ | +1.1241 SD
14위 | ENFJ | +1.1117 SD
15위 | ISFP | +1.0952 SD
16위 | ISTP | +0.9706 SD

==================================================
 [🏆 O 개방성 (Openness) 순위표 (Top to Bottom)]
==================================================
 1위 | INFP | +2.0573 SD
 2위 | ENFP | +1.7159 SD
 3위 | ENFJ | +1.5435 SD
 4위 | ENTP | +1.3726 SD
 5위 | INTP | +1.3374 SD
 6위 | INFJ | +1.2393 SD
 7위 | INTJ | +0.5380 SD
 8위 | ESFP | +0.4274 SD
 9위 | ENTJ | +0.4148 SD
10위 | ISFP | +0.0752 SD
11위 | ESFJ | -0.0613 SD
12위 | ISFJ | -0.1922 SD
13위 | ISTJ | -0.3689 SD
14위 | ESTP | -0.4575 SD
15위 | ISTP | -0.5806 SD
16위 | ESTJ | -0.6257 SD

==================================================
 [🏆 C 성실성 (Conscientiousness) 순위표 (Top to Bottom)]
==================================================
 1위 | ENTJ | +2.3540 SD
 2위 | ESTJ | +2.2992 SD
 3위 | ISTJ | +2.1666 SD
 4위 | INTJ | +1.9985 SD
 5위 | INFJ | +1.8952 SD
 6위 | ISFJ | +1.7637 SD
 7위 | ENFJ | +1.5225 SD
 8위 | ESFJ | +1.4944 SD
 9위 | ISTP | +0.1780 SD
10위 | ENTP | +0.1101 SD
11위 | ESTP | -0.0211 SD
12위 | INTP | -0.2483 SD
13위 | ESFP | -0.5364 SD
14위 | ISFP | -0.7214 SD
15위 | INFP | -0.8292 SD
16위 | ENFP | -1.0423 SD

==================================================
 [🏆 E 외향성 (Extraversion) 순위표 (Top to Bottom)]
==================================================
 1위 | ESFP | +2.7805 SD
 2위 | ESTP | +2.2897 SD
 3위 | ENTP | +2.1281 SD
 4위 | ENFP | +2.0982 SD
 5위 | ESTJ | +2.0311 SD
 6위 | ENTJ | +1.7436 SD
 7위 | ESFJ | +1.6159 SD
 8위 | ENFJ | +1.5961 SD
 9위 | INFP | -0.6227 SD
10위 | ISTP | -0.6378 SD
11위 | INFJ | -0.6638 SD
12위 | ISFP | -0.8516 SD
13위 | ISFJ | -0.8946 SD
14위 | ISTJ | -0.9862 SD
15위 | INTP | -1.2480 SD
16위 | INTJ | -1.3329 SD

==================================================
 [🏆 A 우호성 (Agreeableness) 순위표 (Top to Bottom)]
==================================================
 1위 | ISFJ | +1.9417 SD
 2위 | ESFJ | +1.9388 SD
 3위 | ENFJ | +1.7956 SD
 4위 | INFJ | +1.6531 SD
 5위 | INFP | +1.6159 SD
 6위 | ISFP | +1.6084 SD
 7위 | ENFP | +1.6035 SD
 8위 | ESFP | +1.2755 SD
 9위 | ENTJ | -0.6730 SD
10위 | INTJ | -0.8431 SD
11위 | INTP | -0.9062 SD
12위 | ESTJ | -0.9257 SD
13위 | ISTP | -0.9354 SD
14위 | ISTJ | -0.9888 SD
15위 | ENTP | -1.1377 SD
16위 | ESTP | -1.3897 SD

==================================================
 [🏆 N 신경성 (Neuroticism) 순위표 (Top to Bottom)]
==================================================
 1위 | INFJ | +1.3435 SD
 2위 | ISTJ | +1.0799 SD
 3위 | ISFJ | +1.0267 SD
 4위 | ENFP | +0.9758 SD
 5위 | INFP | +0.9458 SD
 6위 | ISFP | +0.8648 SD
 7위 | INTJ | +0.7261 SD
 8위 | ESTJ | +0.7168 SD
 9위 | INTP | +0.4995 SD
10위 | ESFJ | +0.0578 SD
11위 | ESFP | +0.0439 SD
12위 | ENFJ | -0.0370 SD
13위 | ISTP | -0.0439 SD
14위 | ENTP | -0.1226 SD
15위 | ESTP | -0.2266 SD
16위 | ENTJ | -0.6382 SD

==================================================

 

 

이렇게 해서 엑셀 파일들을 pandas, openpyxl 라이브러리로 데이터들을 읽어내서 통계를 내는 것까지 해냈습니다.

 

 

 

이제 마무리로 들어갑니다.

1. 프로젝트 구조를 다음과 같이 합니다.

2. 각 소스 코드를 다음과 같이 작성합니다.

[sample.py]

더보기
# =====================================================================
# 📋 Big 5 (NEO-PI-R 30 Facets) 테스트 데이터셋
# =====================================================================
BIG5_30_FACETS_DATA = {
    "O 개방성": {
        "상상력(Imagination)": 1.75,
        "예술적 감수성(Artistic Interests)": 0.85,
        "감정 존중(Emotionality)": 2.50,
        "모험성(Adventurousness)": 1.95,
        "지적 호기심(Intellect)": 2.40,
        "가치 진보성(Liberalism)": 0.55,
    },
    "C 성실성": {
        "자신감(Self-efficacy)": 1.65,
        "계획성(Orderliness)": -0.45,
        "책임감(Dutifulness)": 1.35,
        "성취욕(Achievement-striving)": 2.25,
        "자제력(Self-discipline)": -0.05,
        "신중함(Cautiousness)": 1.25,
    },
    "E 외향성": {
        "친밀감(Warmth)": 1.75,
        "사교성(Gregariousness)": 1.55,
        "리더십(Assertiveness)": 1.85,
        "활동성(Activity)": 2.25,
        "흥미 추구(Excitement-seeking)": 2.75,
        "명랑함(Positive-emotions)": 2.15,
    },
    "A 우호성": {
        "신뢰(Trust)": 1.85,
        "정직함(Honesty)": 1.95,
        "이타주의(Altruism)": 2.40,
        "협조성(Cooperation)": 0.75,
        "겸손함(Modesty)": 0.85,
        "공감력(Sympathy)": 1.15,
    },
    "N 신경성": {
        "걱정(Anxiety)": -0.30,
        "분노(Anger)": 1.25,
        "우울(Depression)": 1.75,
        "자의식(Self-consciousness)": -0.35,
        "충동성(Impulsiveness)": 2.35,
        "심약함(Vulnerability)": 1.95,
    },
}

# 규준 기관의 고정 알파 (안전 하한선 기준치)
FIXED_NORM_ALPHAS = {
    "O 개방성": 0.7450,
    "C 성실성": 0.6500,
    "E 외향성": 0.7800,
    "A 우호성": 0.7000,
    "N 신경성": 0.7200,
}

 

[calculate.py]

더보기
from decimal import Decimal, getcontext

# 부동소수점 오차 원천 차단을 위한 고정밀 연산 자릿수 설정 (28자리)
getcontext().prec = 28

class Big5SafeMathEngine:
    """하위 척도 분산 공식의 동적 알파와 규준 고정 알파 중

    안전 하한선 max(Dynamic, Fixed)를 적용하여 Z점수를 산출하는 최종 엔진
    """

    @staticmethod
    def calculate_rmsd_and_mean(facet_scores):
        """[공식 1] 산술 평균(Mean Z) 및 평균 제곱근 편차(RMSD) 연산"""
        scores = [Decimal(str(s)) for s in facet_scores]
        k = Decimal(len(scores))

        if k == 0:
            return Decimal("0"), Decimal("0"), k

        mean_z = sum(scores) / k
        sum_sq_dev = sum((s - mean_z) ** 2 for s in scores)
        rmsd = (sum_sq_dev / k).sqrt()

        return mean_z, rmsd, k

    @classmethod
    def calculate_dynamic_alpha(cls, facet_scores):
        """[공식 2] 분산 분할 법칙 기반 동적 크론바흐 알파 직접 연산"""
        mean_z, rmsd, k = cls.calculate_rmsd_and_mean(facet_scores)

        if k <= 1:
            return Decimal("1.0")

        sum_item_variance = k
        profile_var = rmsd**2
        sigma_x_squared = k + (k * (k - Decimal("1"))) / (
            Decimal("1") + profile_var * Decimal("4")
        )

        alpha = (k / (k - Decimal("1"))) * (
            Decimal("1") - (sum_item_variance / sigma_x_squared)
        )
        alpha = max(Decimal("0.0000"), min(Decimal("1.0000"), alpha))

        return alpha

    @classmethod
    def compile_safe_domain_score(cls, facet_scores, fixed_norm_alpha):
        """[공식 3 - 최종 보정] 안전 하한선 max(Dynamic, Fixed) 기반 Z점수 컴파일

        1. 계산된 동적 알파가 규준보다 높으면 -> 개인의 높은 일관성 그대로 존중
        2. 계산된 동적 알파가 규준보다 낮으면 -> 규준 고정값을 하한선 방파제로 적용
        """
        mean_z, rmsd, k = cls.calculate_rmsd_and_mean(facet_scores)

        if k == 0:
            return (
                Decimal("0"),
                Decimal("50"),
                Decimal("0"),
                Decimal("0"),
                Decimal("0"),
                "N/A",
            )

        # 동적 수학 알파 산출
        dynamic_alpha = cls.calculate_dynamic_alpha(facet_scores)
        fixed_alpha = Decimal(str(fixed_norm_alpha))

        # ★ 핵심 보정 아키텍처: 안전 하한선 max 선택
        if dynamic_alpha >= fixed_alpha:
            safe_alpha = dynamic_alpha
            selection_type = "동적 수학 알파 (고신뢰도 존중)"
        else:
            safe_alpha = fixed_alpha
            selection_type = "규준 고정 알파 (하한선 방어)"

        # 복합 표준편차 가중치 산출: k - (k - 1) * safe_alpha
        inner_weight = k - (k - Decimal("1")) * safe_alpha
        if inner_weight < Decimal("0"):
            inner_weight = Decimal("0")

        z_domain = mean_z * inner_weight.sqrt()
        t_domain = Decimal("50") + (z_domain * Decimal("10"))

        return z_domain, t_domain, safe_alpha, dynamic_alpha, rmsd, selection_type
    
def calculate_Z_domain_values(BIG5_30_FACETS_DATA:dict, FIXED_NORM_ALPHAS:dict) :
    print("=" * 80)
    print(
        " [Big 5 NEO-PI-R 30 Facets 안전 하한선 max(Dynamic, Fixed) 최종 컴파일러]"
    )
    print("=" * 80)

    for domain_name, facets_dict in BIG5_30_FACETS_DATA.items():
        facet_scores = list(facets_dict.values())
        fixed_alpha = FIXED_NORM_ALPHAS[domain_name]

        z_dom, t_dom, safe_alpha, dyn_alpha, rmsd, sel_type = (
            Big5SafeMathEngine.compile_safe_domain_score(facet_scores, fixed_alpha)
        )

        mean_z = sum(facet_scores) / len(facet_scores)

        print(f"\n▶ {domain_name} (하위 척도 k={len(facet_scores)})")
        print("-" * 68)
        print(
            f"  • 산술 평균(Mean Z): {mean_z:+.4f} SD | 내부 편차(RMSD): {rmsd:.4f} SD"
        )
        print(
            f"  • 자체 계산 알파  : {dyn_alpha:.4f} vs 규준 고정 알파: {fixed_alpha:.4f}"
        )
        print(f"  ★ 최종 선택 알파  : {safe_alpha:.4f} ──> [{sel_type}]")
        print(
            f"  ★ 최종 상위 Z점수 : {z_dom:+.4f} SD (T점수 환산: {t_dom:.1f}점)"
        )
    print("\n" + "=" * 80)
    
def calculate_RMSD(BIG5_30_FACETS_DATA:dict):
    print("=" * 80)
    print(" [Big 5 30 Facets 전체 극단성(RMSD) 단일 스칼라 산출 엔진]")
    print("=" * 80)

    # 1. 2차원 딕셔너리에서 30개의 모든 Z점수를 1차원 리스트로 평탄화(Flatten)
    all_scores = []
    for domain_name, facets_dict in BIG5_30_FACETS_DATA.items():
        all_scores.extend(facets_dict.values())

    # 2. 고정밀 연산을 위한 Decimal 변환 및 N 산출
    scores = [Decimal(str(s)) for s in all_scores]
    N = Decimal(len(scores))

    if N == 0:
        print("연산할 데이터가 존재하지 않습니다.")
        return Decimal("0")

    # 3. RMSD 수식 적용: root( sum(Z^2) / N )
    sum_sq = sum(s ** 2 for s in scores)
    rmsd_total = (sum_sq / N).sqrt()

    # 4. 결과 출력
    print(f"▶ 분석 대상 척도 수 (N)  : {int(N)}개")
    print(f"▶ Z점수 제곱의 합 (ΣZ²) : {sum_sq:.4f}")
    print(f"★ 30 척도 통합 RMSD 지수: {rmsd_total:.4f} SD")
    print("=" * 80)

    return rmsd_total

 

[data_loader.py]

더보기
import pandas as pd
import json

def load_big5_databases(db_path="Big5_DB.xlsx", alpha_path="Big5_alpha.xlsx"):
    """
    엑셀 파일들을 읽어들여 시스템에서 사용할 수 있는 Dict(JSON) 형태로 반환하는 로더 함수
    """
    
    # =====================================================================
    # 1. FIXED_NORM_ALPHAS_DB 추출 파이프라인
    # =====================================================================
    df_alpha = pd.read_excel(alpha_path)
    
    # 엑셀의 단순 영문 헤더를 시스템 규격 이름으로 매핑
    domain_mapping = {
        "O": "O 개방성",
        "C": "C 성실성",
        "E": "E 외향성",
        "A": "A 우호성",
        "N": "N 신경성"
    }
    
    # DB의 영문 헤더를 calculate.py가 요구하는 원본 규격으로 복원하는 매퍼 추가
    facet_mapping = {
        "Imagination": "상상력(Imagination)", "Artistic Interests": "예술적 감수성(Artistic Interests)", 
        "Emotionality": "감정 존중(Emotionality)", "Adventurousness": "모험성(Adventurousness)", 
        "Intellect": "지적 호기심(Intellect)", "Liberalism": "가치 진보성(Liberalism)",
        
        "Self-efficacy": "자신감(Self-efficacy)", "Orderliness": "계획성(Orderliness)", 
        "Dutifulness": "책임감(Dutifulness)", "Achievement-striving": "성취욕(Achievement-striving)", 
        "Self-discipline": "자제력(Self-discipline)", "Cautiousness": "신중함(Cautiousness)",
        
        "Warmth": "친밀감(Warmth)", "Gregariousness": "사교성(Gregariousness)", 
        "Assertiveness": "리더십(Assertiveness)", "Activity": "활동성(Activity)", 
        "Excitement-seeking": "흥미 추구(Excitement-seeking)", "Positive-emotions": "명랑함(Positive-emotions)",
        
        "Trust": "신뢰(Trust)", "Honesty": "정직함(Honesty)", 
        "Altruism": "이타주의(Altruism)", "Cooperation": "협조성(Cooperation)", 
        "Modesty": "겸손함(Modesty)", "Sympathy": "공감력(Sympathy)",
        
        "Anxiety": "걱정(Anxiety)", "Anger": "분노(Anger)", 
        "Depression": "우울(Depression)", "Self-consciousness": "자의식(Self-consciousness)", 
        "Impulsiveness": "충동성(Impulsiveness)", "Vulnerability": "심약함(Vulnerability)"
    }
    
    FIXED_NORM_ALPHAS_DB = {}
    for col in df_alpha.columns:
        if col in domain_mapping:
            # 첫 번째 행의 값을 float으로 안전하게 변환하여 저장
            FIXED_NORM_ALPHAS_DB[domain_mapping[col]] = float(df_alpha[col].iloc[0])

    # =====================================================================
    # 2. BIG5_DB_DATA 추출 파이프라인
    # =====================================================================
    # header=None으로 읽어와서 0행(대분류), 1행(소분류)을 직접 컨트롤합니다.
    df_db = pd.read_excel(db_path, header=None)
    
    # 하위 척도(Facet)가 위치한 1행의 값들을 리스트로 추출
    facets_row = df_db.iloc[1].fillna("").astype(str).tolist()
    
    # 5대 도메인이 위치한 Column 인덱스 범위 하드코딩 (아키텍처 구조 고정)
    domain_ranges = {
        "O 개방성": range(7, 13),
        "C 성실성": range(13, 19),
        "E 외향성": range(19, 25),
        "A 우호성": range(25, 31),
        "N 신경성": range(31, 37)
    }
    
    BIG5_DB_DATA = {}
    
    # 결측치(NaN)를 파이썬의 None(JSON의 null)으로 변환해 주는 헬퍼 함수
    def get_value(v):
        if pd.isna(v): 
            return None
        return v
    
    # 실제 데이터가 시작되는 2행(Index 2)부터 끝까지 순회
    for index in range(2, len(df_db)):
        row = df_db.iloc[index]
        user_id = str(row[0])
        
        # ID가 없거나 비어있는 빈 행은 건너뜀 (안전 장치)
        if pd.isna(row[0]) or user_id.strip() == "":
            continue
            
        # 개별 유저 데이터 딕셔너 조립
        user_data = {
            "name": get_value(row[1]),
            "gender": get_value(row[2]),
            "birth date": get_value(row[3]),
            "MBTI": get_value(row[4]),
            "Enneagram": get_value(row[5]),
            "Tritype": get_value(row[6]),
            "BIG5_30_FACETS_DATA": {}
        }
        
        # Z-score 30개 파싱 및 도메인별 딕셔너리 할당
        for domain, col_range in domain_ranges.items():
            user_data["BIG5_30_FACETS_DATA"][domain] = {}
            for col_index in col_range:
                # 1. 원본 영문 헤더 추출 후 공백 제거
                eng_facet_name = facets_row[col_index].strip()
                
                # 2. 매퍼를 통해 원본 한글명으로 변환 (없을 경우 영문명 그대로 사용)
                full_facet_name = facet_mapping.get(eng_facet_name, eng_facet_name)
                
                value = get_value(row[col_index])
                
                # 3. 변환된 full_facet_name(한글+괄호)을 Key로 사용하여 할당
                user_data["BIG5_30_FACETS_DATA"][domain][full_facet_name] = float(value) if value is not None else 0.0
                
        # 고유 ID를 Key로 하여 마스터 딕셔너리에 추가
        BIG5_DB_DATA[user_id] = user_data
        
    return BIG5_DB_DATA, FIXED_NORM_ALPHAS_DB

# =====================================================================
# 테스트 실행 (직접 실행될 때만 작동)
# =====================================================================
if __name__ == "__main__":
    db_data, alpha_data = load_big5_databases()
    
    print("=" * 60)
    print(" [FIXED_NORM_ALPHAS_DB 로드 완료]")
    print("=" * 60)
    print(json.dumps(alpha_data, indent=4, ensure_ascii=False))
    
    print("\n" + "=" * 60)
    print(" [BIG5_DB_DATA 로드 완료 (첫 번째 유저 샘플)]")
    print("=" * 60)
    
    # 전체를 출력하면 너무 길어지므로, 첫 번째 유저(USER_001)만 예시로 출력
    first_user_id = list(db_data.keys())[0]
    first_user_data = {first_user_id: db_data[first_user_id]}
    print(json.dumps(first_user_data, indent=4, ensure_ascii=False))

 

[main.py]

더보기
# 1. 외부 모듈에서 샘플 데이터(상수) 적재
from sample import BIG5_30_FACETS_DATA, FIXED_NORM_ALPHAS

# 2. 외부 모듈에서 데이터 로더 적재
from data_loader import load_big5_databases

# 3. 외부 모듈에서 핵심 연산 엔진(함수) 적재
from calculate import Big5SafeMathEngine, calculate_RMSD, calculate_Z_domain_values

def calculate_single_user_rmsd(facet_scores_dict):
    """특정 유저의 30개 하위 척도 딕셔너리를 받아 RMSD를 반환하는 헬퍼 함수"""
    all_scores = []
    for facets in facet_scores_dict.values():
        all_scores.extend(facets.values())
    
    # 2차원 리스트 평탄화 후 내부 함수인 calculate_rmsd_and_mean 재활용
    # calculate_rmsd_and_mean은 (mean_z, rmsd, k) 튜플을 반환함
    _, rmsd, _ = Big5SafeMathEngine.calculate_rmsd_and_mean(all_scores)
    return float(rmsd)

def sample_data_loading_version() :
    # 파이프라인 1 : 5대 도메인 상위 Z점수 컴파일 및 출력
    calculate_Z_domain_values(BIG5_30_FACETS_DATA, FIXED_NORM_ALPHAS)
    
    # 두 출력 결과 사이의 시각적 간격을 맞추기 위한 빈 줄 출력
    print("\n")
    
    # 파이프라인 2 : 30개 하위 척도 통합 RMSD 컴파일 및 출력
    calculate_RMSD(BIG5_30_FACETS_DATA)

def excel_data_loading_version() :
    # 1. 데이터베이스 로드
    BIG5_DB_DATA, FIXED_NORM_ALPHAS_DB = load_big5_databases("Big5_DB.xlsx", "Big5_alpha.xlsx")
    
    # 결과를 담을 랭킹 리스트 선언
    # 딕셔너리 구조: {"ID": "...", "MBTI": "...", "RMSD": 1.5, "O": 2.1, "C": -1.2, ...}
    ranking_data = []

    # 2. 유저 순회하며 통계 데이터 추출
    for user_id, user_info in BIG5_DB_DATA.items():
        mbti = user_info["MBTI"]
        facets_data = user_info["BIG5_30_FACETS_DATA"]
        
        user_result = {
            "ID": user_id,
            "MBTI": mbti,
        }
        
        # 2-1. 도메인별 Z-score 연산 및 저장
        for domain_name, facets_dict in facets_data.items():
            facet_scores = list(facets_dict.values())
            fixed_alpha = FIXED_NORM_ALPHAS_DB[domain_name]
            
            # z_domain은 튜플의 첫 번째[0] 요소
            z_dom, _, _, _, _, _ = Big5SafeMathEngine.compile_safe_domain_score(facet_scores, fixed_alpha)
            
            # "O 개방성" -> "O" 형태로 키 값 축약
            domain_key = domain_name[0] 
            user_result[domain_key] = float(z_dom)
            
        # 2-2. 통합 RMSD 연산 및 저장
        user_result["RMSD"] = calculate_single_user_rmsd(facets_data)
        
        ranking_data.append(user_result)

    # 3. 순위표 출력 헬퍼 함수 (내림차순 정렬)
    def print_ranking(metric, title):
        # metric(예: "RMSD", "O")을 기준으로 내림차순 정렬
        sorted_data = sorted(ranking_data, key=lambda x: x[metric], reverse=True)
        
        print(f"\n{'='*50}")
        print(f" [🏆 {title} 순위표 (Top to Bottom)]")
        print(f"{'='*50}")
        for rank, data in enumerate(sorted_data, 1):
            score = data[metric]
            print(f"{rank:2d}위 | {data['MBTI']:<4} | {score:+.4f} SD")

    # 4. 최종 결과 출력 파이프라인 가동
    print_ranking("RMSD", "통합 극단성 (RMSD)")
    print_ranking("O", "O 개방성 (Openness)")
    print_ranking("C", "C 성실성 (Conscientiousness)")
    print_ranking("E", "E 외향성 (Extraversion)")
    print_ranking("A", "A 우호성 (Agreeableness)")
    print_ranking("N", "N 신경성 (Neuroticism)")
    print("\n" + "="*50)

# Java에서 main(String args[]) 역할과 동일한 함수입니다. 여기가 실행부입니다.
# 아래에 있는 주석을 해제해서 실행해보세요.
def main():
    # sample_data_loading_version()
    # excel_data_loading_version()
    return

if __name__ == "__main__":
    main()

 

Big5_alpha.xlsx
0.01MB
Big5_DB.xlsx
0.01MB

 

3. 가상 환경(.venv) 설정하고 활성화한 뒤 pandas, openpyxl 외부 라이브러리들을 설치해주세요.

windows powershell 기준으로 다음과 같이 VS Code 터미널에 아래와 같은 절차를 실행해주세요.

- "python -m venv .venv" 이걸로 가상 환경 설정합니다.

- " .\.venv\Scripts\Activate.ps1" 이걸로 가상 환경 활성화 합니다.

- "pip install pandas", "pip install openpyxl" 차례대로 입력해서 외부 라이브러리들을 설치합니다.

* 참고로 다시 원래 전역 환경으로 돌아가고 싶다면 "deactivate" 입력해주세요.

 

4. 마지막으로 main.py에서 main() 함수에 제가 주석 처리한 코드를 주석 해제해서 실행해보세요.

만약 샘플 데이터 분석 결과와 랭킹 결과가 모두 다 나오면 정상적으로 나온 것입니다.

그 외에도 제가 제공한 엑셀 파일에서 하위 척도 값들과 alpha 계수를 살짝 변경해서 어떻게 변하는지 직접 눈으로 확인해보세요.

 

주의사항 : 제가 넣은 엑셀 데이터는 임의로 넣은 것입니다!

실제 데이터와 많이 다를 수 있으므로 해석 결과를 있는 그대로 받아들이면 큰일납니다!

예시로 여기서는 INTJ가 외향성 꼴지로 나오나 실제 외향성 꼴지는 INTP입니다.

라고 했지만 그래봐야 1단계 차이일 뿐이고 거의 대부분 일치한 결과가 나와서 유의미한 결과로 보입니다.

 

 

이렇게 해서 오늘 논문을 읽어내서 Big 5에 대해 간단하게 알아보고

수학적인 공식 유도로 크론바흐 알파 계수가 적용된 상위 요인의 Z 점수를 구하는 것과

RMSD 공식을 적용해서 Python 소스 코드로 구현해서

외부 라이브러리들을 활용해서 엑셀 파일들을 읽어내서 데이터 분석을 마쳤습니다.

 

긴 글을 읽어주셔서 감사합니다.