Comment utiliser la décomposition de Cholesky, ou une alternative, pour la simulation de données corrélées

19

J'utilise la décomposition de Cholesky pour simuler des variables aléatoires corrélées étant donné une matrice de corrélation. Le fait est que le résultat ne reproduit jamais la structure de corrélation telle qu'elle est donnée. Voici un petit exemple en Python pour illustrer la situation.

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))) 

Cela imprime:

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

Comme vous pouvez le voir, la matrice de corrélation estimée post-hoc diffère radicalement de la précédente. Y a-t-il un bogue dans mon code ou existe-t-il une alternative à l'utilisation de la décomposition Cholesky?

Éditer

Je vous demande pardon pour ce gâchis. Je ne pensais pas qu'il y avait une erreur dans le code et / ou dans la façon dont la décomposition de Cholesky était appliquée en raison d'une mauvaise compréhension du matériel que j'avais étudié auparavant. En fait, j'étais sûr que la méthode elle-même n'était pas censée être précise et j'étais d'accord avec cela jusqu'à la situation qui m'a fait poser cette question. Merci d'avoir souligné l'idée fausse que j'avais. J'ai édité le titre pour mieux refléter la situation réelle proposée par @Silverfish.

Eli Korvigo
la source
1
Cholesky fonctionne très bien, et c'est vraiment une question de type "pouvez-vous trouver le bug dans mon code". Le titre et le contenu de la question, tels qu'ils sont rédigés à l'origine, sont essentiellement "Cholesky ne fonctionne pas, quelle est l'alternative"? Ce sera très déroutant pour les utilisateurs qui recherchent ce site. Cette question doit-elle être modifiée pour refléter cela? (L'inconvénient est que la réponse de javlacalle serait moins pertinente. L'avantage est que le texte de la question refléterait alors ce que les chercheurs trouveraient réellement sur la page.)
Silverfish
@Antoni Parellada Oui, je pense que vous avez traduit mon code MATLAB pour (a) la façon correcte de le faire en numpy Python, avec un ajustement pour que np.linalg.cholesky soit triangulaire inférieur par rapport au chol de MATLAB étant triangulaire supérieur. J'avais déjà traduit le code incorrect de l'OP en son équivalent MATLAB et dupliqué ses résultats incorrects.
Mark L. Stone

Réponses:

11

L'approche basée sur la décomposition de Cholesky devrait fonctionner, elle est décrite ici et est montrée dans la réponse de Mark L. Stone postée presque en même temps que cette réponse.

Néanmoins, j'ai parfois généré des tirages à partir de la distribution normale multivariée comme suit:N(μ,Σ)

Oui=QX+μ,avecQ=Λ1/2Φ,

où sont les tirages finaux, sont des tirages de la distribution normale standard univariée, est une matrice contenant les vecteurs propres normalisés de la matrice cible et est une matrice diagonale contenant les valeurs propres de disposées dans le même ordre comme vecteurs propres dans les colonnes de .X Φ Σ Λ Σ ΦOuiXΦΣΛΣΦ

Exemple dans R(désolé, je n'utilise pas le même logiciel que vous avez utilisé dans la question):

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

Vous pouvez également être intéressé par cet article et cet article .

javlacalle
la source
Pour rendre la matrice de corrélation reproduite précise, il convient de supprimer les corrélations parasites dans les données aléatoires du générateur aléatoire avant de l'appliquer à la procédure de génération de données. Par exemple, vérifiez la corrélation de vos données aléatoires en eps pour voir d'abord ces fausses corrélations.
Gottfried Helms
17

Les gens trouveraient probablement votre erreur beaucoup plus rapidement si vous expliquiez ce que vous avez fait avec des mots et l'algèbre plutôt qu'avec du code (ou du moins l'écrivant en utilisant un pseudocode).

Vous semblez faire l'équivalent de ceci (bien que possiblement transposé):

  1. n×kZ

  2. σjeμje

  3. Oui=LX

L

Ce que vous devez faire est le suivant:

  1. n×kZ

  2. X=LZ

  3. σjeμje

Il existe de nombreuses explications sur cet algorithme sur le site. par exemple

Comment générer des nombres aléatoires corrélés (moyennes, variances et degrés de corrélation donnés)?

Puis-je utiliser la méthode Cholesky pour générer des variables aléatoires corrélées avec une moyenne donnée?

Celui-ci en parle directement en termes de matrice de covariance souhaitée, et donne également un algorithme pour obtenir une covariance d' échantillon souhaitée :

Génération de données avec une matrice de covariance d'échantillon donnée

Glen_b -Reinstate Monica
la source
11

Il n'y a rien de mal à la factorisation de Cholesky. Il y a une erreur dans votre code. Voir modification ci-dessous.

Voici le code MATLAB et les résultats, d'abord pour n_obs = 10000 comme vous l'avez, puis pour n_obs = 1e8. Pour plus de simplicité, car cela n'affecte pas les résultats, je ne me soucie pas des moyens, c'est-à-dire que je leur fais des zéros. Notez que le chol de MATLAB produit un facteur de Cholesky triangulaire supérieur R de la matrice M tel que R '* R = M. numpy.linalg.cholesky produit un facteur de Cholesky triangulaire inférieur, donc un ajustement par rapport à mon code est nécessaire; mais je crois que votre code est très bien à cet égard.

   >> 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: j'ai trouvé votre erreur. Vous avez incorrectement appliqué l'écart type. C'est l'équivalent de ce que vous avez fait, ce qui est faux.

   >> 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
la source
6

CV ne concerne pas le code, mais j'étais intrigué de voir à quoi cela ressemblerait après toutes les bonnes réponses, et en particulier la contribution de @Mark L. Stone. La réponse réelle à la question est fournie sur son message (veuillez créditer son message en cas de doute). Je déplace ces informations jointes ici pour faciliter la récupération de ce message à l'avenir. Sans minimiser aucune des autres excellentes réponses, après la réponse de Mark, cela résout le problème en corrigeant le message dans l'OP.

La source

À PYTHON:

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.        ]]

EN [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
la source
1

Comme d'autres l'ont déjà montré: des œuvres cholesky. Voici un bout de code qui est très court et très proche du pseudocode: un codepiece dans 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
Gottfried Helms
la source