Como usar a decomposição de Cholesky, ou uma alternativa, para simulação de dados correlacionados

19

Uso a decomposição de Cholesky para simular variáveis ​​aleatórias correlacionadas, dada uma matriz de correlação. A questão é que o resultado nunca reproduz a estrutura de correlação conforme é fornecida. Aqui está um pequeno exemplo em Python para ilustrar a situação.

import numpy as np    

n_obs = 10000
means = [1, 2, 3]
sds = [1, 2, 3] # standard deviations 

# generating random independent variables 
observations = np.vstack([np.random.normal(loc=mean, scale=sd, size=n_obs)
                   for mean, sd in zip(means, sds)])  # observations, a row per variable

cor_matrix = np.array([[1.0, 0.6, 0.9],
                       [0.6, 1.0, 0.5],
                       [0.9, 0.5, 1.0]])

L = np.linalg.cholesky(cor_matrix)

print(np.corrcoef(L.dot(observations))) 

Isso imprime:

[[ 1.          0.34450587  0.57515737]
 [ 0.34450587  1.          0.1488504 ]
 [ 0.57515737  0.1488504   1.        ]]

Como você pode ver, a matriz de correlação estimada post-hoc difere drasticamente da matriz anterior. Existe um erro no meu código ou existe alguma alternativa ao uso da decomposição de Cholesky?

Editar

Peço perdão por essa bagunça. Não achei que houvesse um erro no código e / ou na maneira como a decomposição de Cholesky era aplicada devido a algum mal-entendido do material que eu havia estudado antes. Na verdade, eu tinha certeza de que o método em si não era para ser preciso e eu estava bem com isso até a situação que me levou a postar essa pergunta. Obrigado por apontar o equívoco que eu tive. Editei o título para refletir melhor a situação real, como proposto por @Silverfish.

Eli Korvigo
fonte
1
Cholesky funciona muito bem, e essa é realmente uma pergunta do tipo "você pode encontrar o bug no meu código"? O título e o conteúdo da pergunta, como está escrito originalmente, são basicamente "Cholesky não funciona, o que é uma alternativa"? Isso será muito confuso para os usuários que pesquisam neste site. Esta pergunta deve ser editada para refletir isso? (A desvantagem é a resposta que do javlacalle seria menos relevante A vantagem é o texto da pergunta, então, refletir o que os pesquisadores realmente iria encontrar na página..)
Silverfish
@Antoni Parellada Sim, acho que você traduziu meu código MATLAB para (a) a maneira correta de fazê-lo em numpy Python, completo com o ajuste para np.linalg.cholesky ser triangular inferior versus chol do MATLAB ser triangular superior. Eu já havia traduzido o código incorreto do OP para seu equivalente MATLAB e duplicado seus resultados incorretos.
Mark L. Stone

Respostas:

11

A abordagem baseada na decomposição de Cholesky deve funcionar, é descrita aqui e é mostrada na resposta de Mark L. Stone postada quase ao mesmo tempo que esta resposta.

N(μ,Σ)

Y=QX+μ,comQ=Λ1/2Φ,

YXΦΣΛΣΦ

Exemplo em R(desculpe, eu não estou usando o mesmo software que você usou na pergunta):

n <- 10000
corM <- rbind(c(1.0, 0.6, 0.9), c(0.6, 1.0, 0.5), c(0.9, 0.5, 1.0))
set.seed(123)
SigmaEV <- eigen(corM)
eps <- rnorm(n * ncol(SigmaEV$vectors))
Meps <- matrix(eps, ncol = n, byrow = TRUE)    
Meps <- SigmaEV$vectors %*% diag(sqrt(SigmaEV$values)) %*% Meps
Meps <- t(Meps)
# target correlation matrix
corM
#      [,1] [,2] [,3]
# [1,]  1.0  0.6  0.9
# [2,]  0.6  1.0  0.5
# [3,]  0.9  0.5  1.0
# correlation matrix for simulated data
cor(Meps)
#           [,1]      [,2]      [,3]
# [1,] 1.0000000 0.6002078 0.8994329
# [2,] 0.6002078 1.0000000 0.5006346
# [3,] 0.8994329 0.5006346 1.0000000

Você também pode estar interessado neste post e neste post .

javlacalle
fonte
Para tornar precisa a matriz de correlação reproduzida, deve-se remover as correlações espúrias nos dados aleatórios do gerador aleatório antes de aplicá-las ao procedimento de geração de dados. Por exemplo, verifique a correlação de seus dados aleatórios em eps para ver primeiro essas correlações espúrias.
Gottfried Helms
17

As pessoas provavelmente encontrarão seu erro muito mais rápido se você explicar o que fez com palavras e álgebra em vez de código (ou pelo menos escrevê-lo usando pseudocódigo).

Você parece estar fazendo o equivalente a isso (embora possivelmente transposto):

  1. n×kZ

  2. σEuμEu

  3. Y=euX

eu

O que você deve fazer é o seguinte:

  1. n×kZ

  2. X=euZ

  3. σEuμEu

Há muitas explicações sobre esse algoritmo no local. por exemplo

Como gerar números aleatórios correlacionados (médias, variações e grau de correlação)?

Posso usar o método de Cholesky para gerar variáveis ​​aleatórias correlacionadas com determinada média?

Este discute-o diretamente em termos da matriz de covariância desejada e também fornece um algoritmo para obter uma covariância de amostra desejada :

Gerando dados com uma determinada matriz de covariância de amostra

Glen_b -Reinstate Monica
fonte
11

Não há nada de errado com a fatoração de Cholesky. Há um erro no seu código. Veja a edição abaixo.

Aqui estão o código e os resultados do MATLAB, primeiro para n_obs = 10000 como você possui, depois para n_obs = 1e8. Por simplicidade, como não afeta os resultados, não me preocupo com os meios, ou seja, os faço zeros. Observe que o chol do MATLAB produz um fator Cholesky triangular superior R da matriz M, de modo que R '* R = M. numpy.linalg.cholesky produz um fator Cholesky triangular inferior, sendo necessário um ajuste em relação ao meu código; mas acredito que seu código é bom nesse sentido.

   >> correlation_matrix = [1.0, 0.6, 0.9; 0.6, 1.0, 0.5;0.9, 0.5, 1.0];
   >> SD = diag([1 2 3]);
   >> covariance_matrix = SD*correlation_matrix*SD
   covariance_matrix =
      1.000000000000000   1.200000000000000   2.700000000000000
      1.200000000000000   4.000000000000000   3.000000000000000
      2.700000000000000   3.000000000000000   9.000000000000000
   >> n_obs = 10000;
   >> Random_sample = randn(n_obs,3)*chol(covariance_matrix);
   >> disp(corr(Random_sample))
      1.000000000000000   0.599105015695768   0.898395949647890
      0.599105015695768   1.000000000000000   0.495147514173305
      0.898395949647890   0.495147514173305   1.000000000000000
   >> n_obs = 1e8;
   >> Random_sample = randn(n_obs,3)*chol(covariance_matrix);
   >> disp(corr(Random_sample))
      1.000000000000000   0.600101477583914   0.899986072541418
      0.600101477583914   1.000000000000000   0.500112824962378
      0.899986072541418   0.500112824962378   1.000000000000000

Edit: Encontrei seu erro. Você aplicou incorretamente o desvio padrão. Isso é o equivalente ao que você fez, o que está errado.

   >> n_obs = 10000;
   >> Random_sample = randn(n_obs,3)*SD*chol(correlation_matrix);
   >> disp(corr(Random_sample))
      1.000000000000000   0.336292731308138   0.562331469857830
      0.336292731308138   1.000000000000000   0.131270077244625
      0.562331469857830   0.131270077244625   1.000000000000000
   >> n_obs=1e8;
   >> Random_sample = randn(n_obs,3)*SD*chol(correlation_matrix);
   >> disp(corr(Random_sample))
      1.000000000000000   0.351254525742470   0.568291702131030
      0.351254525742470   1.000000000000000   0.140443281045496
      0.568291702131030   0.140443281045496   1.000000000000000
Mark L. Stone
fonte
6

O CV não é sobre código, mas fiquei intrigado ao ver como isso ficaria depois de todas as boas respostas, e especificamente da contribuição de Mark L. Stone. A resposta real à pergunta é fornecida em sua postagem (credite sua postagem em caso de dúvida). Estou movendo essas informações anexadas aqui para facilitar a recuperação desta postagem no futuro. Sem descartar nenhuma das outras excelentes respostas, após a resposta de Mark, isso encerra o problema, corrigindo a postagem no OP.

Fonte

EM PITÃO:

import numpy as np

no_obs = 1000             # Number of observations per column
means = [1, 2, 3]         # Mean values of each column
no_cols = 3               # Number of columns

sds = [1, 2, 3]           # SD of each column
sd = np.diag(sds)         # SD in a diagonal matrix for later operations

observations = np.random.normal(0, 1, (no_cols, no_obs)) # Rd draws N(0,1) in [3 x 1,000]

cor_matrix = np.array([[1.0, 0.6, 0.9],
                       [0.6, 1.0, 0.5],
                       [0.9, 0.5, 1.0]])          # The correlation matrix [3 x 3]

cov_matrix = np.dot(sd, np.dot(cor_matrix, sd))   # The covariance matrix

Chol = np.linalg.cholesky(cov_matrix)             # Cholesky decomposition

array([[ 1.        ,  0.        ,  0.        ],
       [ 1.2       ,  1.6       ,  0.        ],
       [ 2.7       , -0.15      ,  1.29903811]])

sam_eq_mean = Chol .dot(observations)             # Generating random MVN (0, cov_matrix)

s = sam_eq_mean.transpose() + means               # Adding the means column wise
samples = s.transpose()                           # Transposing back

print(np.corrcoef(samples))                       # Checking correlation consistency.

[[ 1.          0.59167434  0.90182308]
 [ 0.59167434  1.          0.49279316]
 [ 0.90182308  0.49279316  1.        ]]

EM R]:

no_obs = 1000             # Number of observations per column
means = 1:3               # Mean values of each column
no_cols = 3               # Number of columns

sds = 1:3                 # SD of each column
sd = diag(sds)         # SD in a diagonal matrix for later operations

observations = matrix(rnorm(no_cols * no_obs), nrow = no_cols) # Rd draws N(0,1)

cor_matrix = matrix(c(1.0, 0.6, 0.9,
                      0.6, 1.0, 0.5,
                      0.9, 0.5, 1.0), byrow = T, nrow = 3)     # cor matrix [3 x 3]

cov_matrix = sd %*% cor_matrix %*% sd                          # The covariance matrix

Chol = chol(cov_matrix)                                        # Cholesky decomposition

     [,1] [,2]      [,3]
[1,]    1  1.2  2.700000
[2,]    0  1.6 -0.150000
[3,]    0  0.0  1.299038

sam_eq_mean = t(observations) %*% Chol          # Generating random MVN (0, cov_matrix)

samples = t(sam_eq_mean) + means

cor(t(samples))

          [,1]      [,2]      [,3]
[1,] 1.0000000 0.6071067 0.8857339
[2,] 0.6071067 1.0000000 0.4655579
[3,] 0.8857339 0.4655579 1.0000000

colMeans(t(samples))
[1] 1.035056 2.099352 3.065797
apply(t(samples), 2, sd)
[1] 0.9543873 1.9788250 2.8903964
Antoni Parellada
fonte
1

Como outros já mostraram: obras cholesky. Aqui, um trecho de código muito curto e muito próximo ao pseudocódigo: uma peça de código no MatMate:

Co = {{1.0, 0.6, 0.9},  _
      {0.6, 1.0, 0.5},  _
      {0.9, 0.5, 1.0}}           // make correlation matrix


chol = cholesky(co)              // do cholesky-decomposition           
data = chol * unkorrzl(randomn(3,100,0,1))  
                                 // dot-multiply cholesky with random-
                                 // vectors with mean=0, sdev=1  
                                 //(refined by a "decorrelation" 
                                 //to remove spurious/random correlations)   


chk = data *' /100               // check the correlation of the data
list chk

1.0000  0.6000  0.9000
0.6000  1.0000  0.5000
0.9000  0.5000  1.0000
Elmos de Gottfried
fonte