본문 바로가기
수학의 재미/행렬 이론

상관관계를 가지는 표준정규분포 변수 여러개 만들기(python)

by hustler78 2022. 9. 20.
728x90
반응형

 

지난 글

2022.09.20 - [수학의 재미/행렬 이론] - n차원 상관계수행렬을 촐레스키 분해하면?

 

n차원 상관계수행렬을 촐레스키 분해하면?

지난 글 2022.09.19 - [수학의 재미/행렬 이론] - 촐레스키 분해 촐레스키 분해 이번 글은 2022.09.16 - [분류 전체보기] - 상관계수 행렬은 positive definite! 상관계수 행렬은 positive definite! 이번 글은 20..

sine-qua-none.tistory.com

에서 상관계수를 가지는 $n$개의 표준정규분포를 만들어 보았습니다. 복습을 간단히 해보겠습니다.

 

 

상관관계를 갖는 표준정규분포 만들기(복습)

1. 서로 독립인 표준정규분포 $n$개, $Z_1, Z_2, \cdots, Z_n$ 을 준비한다.

2. 상관계수 행렬 $\mathbf{R}$를 $\mathbf{R} = \mathbf{L}\mathbf{L}^t$로 촐레스키 분해한다.

3. 다음의 행렬을 만족하는 $X_1, X_2, \cdots, X_n$이 우리가 원하는 상관계수를 가지는 표준정규분포이다.

$$ \begin{pmatrix} X_1\\X_2\\ \vdots \\ X_n \end{pmatrix} = \mathbf{L} \begin{pmatrix} Z_1\\Z_2\\ \vdots \\ Z_n \end{pmatrix} \tag{1}$$

 

 

 

Python Code : 촐레스키 분해

상관관계를 갖는 표준정규분포를 만들기 위해서는 촐레스키 분해를 할 줄 알아야 합니다. Python에서는 촐레스키 분해 함수가 있습니다. 아래 예제를 볼까요?

 

import pprint
import scipy
import scipy.linalg
import numpy as np

CorrMat = np.array([[1, 0.8, 0.6],
                    [0.8, 1, 0.4],
                    [0.6, 0.4, 1]])
L_scipy = scipy.linalg.cholesky(CorrMat, lower=True)
U_scipy = scipy.linalg.cholesky(CorrMat, lower=False)

L_np = np.linalg.cholesky(CorrMat)
U_np = L_np.transpose()

print("Correlation Matrix:")
pprint.pprint(CorrMat)

print("L_scipy:")
pprint.pprint(L_scipy)

print("U_scipy")
pprint.pprint(U_scipy)

print("L_np:")
pprint.pprint(L_np)

print("U_np")
pprint.pprint(U_np)

 

Python으로 촐레스키 분해를 할 수 있는 방법은 2가지가 있습니다.

 

scipy.linalg.cholesky(A, lower=True)

scipy라는 수학/공학용 오픈소스 내, linalg(linear algebra) 라이브러리 내 Cholesky 함수입니다.

행렬 A를 input으로 받고, lower = True 이면 하삼각행렬을, lower = False 이면 상삼각행렬을 반환합니다.

 

 

numpy.linalg.cholesky(A)

numpy 라이브러리 내, linalg 내 Cholesky 함수입니다. 행렬 A를 받아 촐레스키 분해하여 상삼각행렬을 반환합니다.

 

 

코드를 간단하게 보겠습니다.

import pprint	# array 출력을 예쁘게 하기위한 print 모듈
import scipy	# scipy.linalg.cholesky 를 쓰기 위해 import
import scipy.linalg
import numpy as np	# np.linalg.cholesky 를 쓰기 위해 import

 

 


CorrMat = np.array([[1, 0.8, 0.6],
                    [0.8, 1, 0.4],
                    [0.6, 0.4, 1]])

# 분석 대상이 되는 3*3 correlation matrix (확률변수 3개)
# 이 행렬이 positive semidefinite가 아니라면 Cholesky 분해는 오류가 남

 

 


L_scipy = scipy.linalg.cholesky(CorrMat, lower=True)	# scipy 를 이용한 cholesky 분해(하삼각행렬 제공)
U_scipy = scipy.linalg.cholesky(CorrMat, lower=False)

L_np = np.linalg.cholesky(CorrMat)	# numpy를 이용한 Cholesky 분해
U_np = L_np.transpose()

 

 


print("Correlation Matrix:")
pprint.pprint(CorrMat)	# CorrMat를 아름답게 출력해줌 (pprint.pprint)

 

 

결과는 다음과 같습니다.

 

Correlation Matrix:
array([[1. , 0.8, 0.6],
       [0.8, 1. , 0.4],
       [0.6, 0.4, 1. ]])
L_scipy:
array([[ 1.        ,  0.        ,  0.        ],
       [ 0.8       ,  0.6       ,  0.        ],
       [ 0.6       , -0.13333333,  0.78881064]])
U_scipy
array([[ 1.        ,  0.8       ,  0.6       ],
       [ 0.        ,  0.6       , -0.13333333],
       [ 0.        ,  0.        ,  0.78881064]])
L_np:
array([[ 1.        ,  0.        ,  0.        ],
       [ 0.8       ,  0.6       ,  0.        ],
       [ 0.6       , -0.13333333,  0.78881064]])
U_np
array([[ 1.        ,  0.8       ,  0.6       ],
       [ 0.        ,  0.6       , -0.13333333],
       [ 0.        ,  0.        ,  0.78881064]])

○ 기호 L은 하삼각행렬(Lower triangular matrix), U는 상삼각행렬(Upper triangular matrix)입니다. scipy와 numpy의 결과가 모두 똑같게 나옵니다.

 

 

 

Python Code : 상관관계가 있는 여러 개의 표준정규분포 난수 만들기

위의 예제에서 쓴

CorrMat = np.array([[1, 0.8, 0.6],
                    [0.8, 1, 0.4],
                    [0.6, 0.4, 1]])

를 상관계수로 하는 3개의 확률변수를 만들어 보도록 합시다. Python Code는 아래와 같습니다.

 

import pprint
import scipy
import scipy.linalg
import numpy as np

import scipy.stats as stats

CorrMat = np.array([[1, 0.8, 0.6],
                    [0.8, 1, 0.4],
                    [0.6, 0.4, 1]])
L_scipy = scipy.linalg.cholesky(CorrMat, lower=True)
U_scipy = scipy.linalg.cholesky(CorrMat, lower=False)

L_np = np.linalg.cholesky(CorrMat)
U_np = L_np.transpose()

dataSet = np.random.normal(size=(10000, 3))

print('Dataset: ')
pprint.pprint(dataSet)
print('\n')

dataSet -= np.mean(dataSet, axis=0)
dataSet /= np.sqrt(np.var(dataSet, axis=0))

mean = np.mean(dataSet, axis=0)
var = np.var(dataSet, axis=0)

print('Mean of dataset:')
pprint.pprint(mean)
print('\n')

print('Variance of dataset: ')
pprint.pprint(var)
print('\n')

corr = np.corrcoef(dataSet.T)
print('correlation matrix of independent standard normal random')
pprint.pprint(corr)


corr_dataSet = dataSet.copy()
corr_dataSet[:, 0] = L_scipy[0][0] * dataSet[:, 0]
corr_dataSet[:, 1] = L_scipy[1][0] * dataSet[:, 0] + L_scipy[1][1] * dataSet[:, 1]
corr_dataSet[:, 2] = L_scipy[2][0] * dataSet[:, 0] + L_scipy[2][1] * dataSet[:, 1] + L_scipy[2][2] * dataSet[:, 2]

print('Mean of correlated dataset: ')
pprint.pprint(np.mean(corr_dataSet, axis=0))
print('\n')

print('Variance of correlated dataset: ')
pprint.pprint(np.var(corr_dataSet, axis=0))
print('\n')

print('Correlation matrix')
pprint.pprint(np.corrcoef(corr_dataSet.T))
print('\n')


print('Normal random test(1st column of dataset: ', stats.jarque_bera(corr_dataSet[:,0]))
print('Normal random test(2nd column of dataset: ', stats.jarque_bera(corr_dataSet[:,1]))
print('Normal random test(3rd column of dataset: ', stats.jarque_bera(corr_dataSet[:,2]))

 

간단하게 살펴보겠습니다.

 

dataSet = np.random.normal(size=(10000, 3))
# 변수 3개이고 각 변수당 샘플 갯수가 10000개인 데이터셋 준비

○ 변수를 $X_1, X_2, X_3$이라 하고 각각이 10,000개의 샘플을 가지고 있는 상황입니다. 즉, 데이터셋은

$$ \begin{pmatrix} \mid & \mid &\mid // X_1 & X_2 & X_3\\ \mid & \mid &\mid $$

입니다.

numpy.random.normal 을 사용하여 표준정규분포 난수를 만듭니다.

 


dataSet -= np.mean(dataSet, axis=0)		# 각 열의 평균을 구해 각 열에서 각각의 평균을 빼줌
dataSet /= np.sqrt(np.var(dataSet, axis=0))	# 각 열의 분산을 구해 각 열을 표준편차로 나눠줌

○ 보다 정확한 Z-score을 구하는 작업입니다.

 


mean = np.mean(dataSet, axis=0)
var = np.var(dataSet, axis=0)
# Z- score 로 normalizing이 잘되었는지 확인

 

 


corr = np.corrcoef(dataSet.T)	# correlation matrix를 구해봄
print('correlation matrix of independent standard normal random')
pprint.pprint(corr)

numpy.corrcoef를 이용해 상관계수 행렬을 구할 수 있습니다. 주의할 점은 변수가 행의 위치, 변수의 샘플이 열의 위치로 와야 하므로 dataSet에 transpose를 취해야 합니다.

 


corr_dataSet = dataSet.copy()	# dataSet의 복사본 만듬. 이 데이터셋을 상관계수 있는 데이터셋으로 만들 예정

# LX 를 하여 상관관계있는 데이터셋 만듬
corr_dataSet[:, 0] = L_scipy[0][0] * dataSet[:, 0]	
corr_dataSet[:, 1] = L_scipy[1][0] * dataSet[:, 0] + L_scipy[1][1] * dataSet[:, 1]
corr_dataSet[:, 2] = L_scipy[2][0] * dataSet[:, 0] + L_scipy[2][1] * dataSet[:, 1] + L_scipy[2][2] * dataSet[:, 2]

○ 상관관계있는 표준정규분포는

$$L\begin{pmatrix} X_1\\X_2\\X_3 \end{pmatrix}$$ 로 얻어집니다. 이 행렬 연산을 직접 한 것입니다.

 

 


print('Mean of correlated dataset: ')
pprint.pprint(np.mean(corr_dataSet, axis=0))	# corr_dataSet의 평균 구하기
print('\n')

print('Variance of correlated dataset: ')
pprint.pprint(np.var(corr_dataSet, axis=0))		# 분산구하기
print('\n')

print('Correlation matrix')
pprint.pprint(np.corrcoef(corr_dataSet.T))		# 상관계수행렬 구하기
print('\n')

○ 새롭게 얻은 데이터셋 세 변수의 평균, 분산, 상관계수 행렬을 구해봅니다. 예상대로라면, 상관계수행렬은 초기에 주어진 행렬과 같게 나와야 하죠.

 

 


# Jarque_bera 검정으로 각 변수가 표준정규분포를 잘 따르는지 확인

print('Normal random test(1st column of dataset: ', stats.jarque_bera(corr_dataSet[:,0]))
print('Normal random test(2nd column of dataset: ', stats.jarque_bera(corr_dataSet[:,1]))
print('Normal random test(3rd column of dataset: ', stats.jarque_bera(corr_dataSet[:,2]))

○ 마지막으로, 새롭게 구한 데이터셋의 각 열이 표준정규 분포를 따르는지 확인해 봅니다.  해당 사항은

2022.06.17 - [수학의 재미/확률분포] - 무엇이 더 정규분포인가? Jarque-Bera Test를 참고해 보시기 바랍니다.

 

jarque_bera()를 사용하기 위해 다음의 import가 필요합니다.

import scipy.stats as stats

 

 

이제 결과를 볼까요?

Dataset: 
array([[-0.5261694 , -1.0196472 ,  0.02098029],
       [-0.00287988,  0.22709799, -0.64654555],
       [ 0.39518587,  0.41786458,  0.83072939],
       ...,
       [ 0.94961726,  0.34046586,  1.08219931],
       [ 1.08533901,  2.5843262 , -0.8059873 ],
       [-1.45422358,  1.2263708 , -0.02244433]])


Mean of dataset:
array([ 2.45581333e-17,  1.97397654e-17, -4.59774580e-17])


Variance of dataset: 
array([1., 1., 1.])


correlation matrix of independent standard normal random
array([[1.00000000e+00, 2.93401986e-03, 2.40809137e-04],
       [2.93401986e-03, 1.00000000e+00, 1.32450244e-02],
       [2.40809137e-04, 1.32450244e-02, 1.00000000e+00]])
Mean of correlated dataset: 
array([ 2.45581333e-17,  5.34239319e-17, -1.31450406e-17])


Variance of correlated dataset: 
array([1.        , 1.00281666, 0.99697242])


Correlation matrix
array([[1.        , 0.80063365, 0.60070879],
       [0.80063365, 1.        , 0.40720862],
       [0.60070879, 0.40720862, 1.        ]])


Normal random test(1st column of dataset:  Jarque_beraResult(statistic=0.8269825617967153, pvalue=0.6613373006224426)
Normal random test(2nd column of dataset:  Jarque_beraResult(statistic=4.213901370438609, pvalue=0.12160822338450805)
Normal random test(3rd column of dataset:  Jarque_beraResult(statistic=1.8961651732749543, pvalue=0.38748327723696296)

○ 원래 데이터의 평균, 분산, 상관계수행렬이 정확히 나왔습니다. 원래 데이터는 3 확률변수가 서로 독립이므로 상관계수행렬은 단위행렬이 나옵니다.

 

○ 새롭게 얻은 데이터셋의 상관계수 행렬을 보세요. 처음에 우리가 가정했던 행렬과 차이가 거의 나지 않습니다.

 

○ Jarque-Bera 검정을 보면, 통계량이 모두 5.99보다 작죠( 여기 참고.) 따라서 표준정규분포라 가정할 수 있습니다.

 

 

 

결론

위와 같이 서로 독립인 표준정규분포에서 서로 상관관계가 있는(우리가 원하는 상관계수를 가지는) 새로운 표준정규분포 난수를 얻을 수 있습니다. 금융공학에서는 여러 개의 금융 자산이 한 데 모여 포트폴리오를 이루는데, 이 포트폴리오의 자산가치의 변동은 금융자산 각각의 움직임도 중요하지만, 서로 어떤 경향으로 움직이는 지도 아주 중요합니다.

위에서 살펴본 원리를 이용하여 여러 개의 금융자산이 움직이는 것을 어떻게 모델링하는지 차차 알아보기로 하겠습니다.

 

 

 

 

 

728x90
반응형

댓글