Как создать большую матрицу случайной корреляции полного ранга с некоторыми сильными корреляциями?


25

Я хотел бы сгенерировать матрицу случайной корреляции размером , чтобы присутствовали умеренно сильные корреляции: n × nCn×n

  • квадратная вещественная симметричная матрица размера , например ;n = 100n×nn=100
  • положительно определенный, то есть со всеми собственными значениями, действительными и положительными;
  • полный ранг;
  • все диагональные элементы равны ;1
  • недиагональные элементы должны быть равномерно распределены по . Точное распределение не имеет значения, но я хотел бы иметь умеренно большое количество (например, ) умеренно больших значений (например, с абсолютным значением или выше). По сути, я хочу убедиться, что не является почти диагональным со всеми недиагональными элементами .10 % 0,5 С(1,1)10%0.5C0

Есть ли простой способ сделать это?

Цель состоит в том, чтобы использовать такие случайные матрицы для сравнения некоторых алгоритмов, работающих с корреляционными (или ковариационными) матрицами.


Методы, которые не работают

Вот несколько способов генерирования матриц случайной корреляции, о которых я знаю, но которые здесь не работают:

  1. Генерация случайных из размера, в центре, стандартизировать и образуют матрицу корреляции . Если , это обычно приводит к тому, что все недиагональные корреляции будут около . Если , некоторые корреляции будут сильными, но не будет полного ранга. s × n C = 1Xs×ns>n0snCC=1s1XXs>n0snC

  2. Генерация случайной положительно определенной матрицы одним из следующих способов:B

    • Создайте случайный квадрат и сделайте симметричный положительно определенный .B = A AAB=AA

    • Создайте случайный квадрат , сделайте симметричный и сделайте его положительно определенным, выполнив собственное разложение и установка всех отрицательных собственных значений в ноль: . NB: это приведет к матрице с недостатком ранга.E = A + AE = U S UB = UAE=A+AE=USUB=Umax{S,0}U

    • Генерация случайной ортогональной (например, путем генерации случайного квадрата и выполнения его QR-разложения или с помощью процесса Грамма-Шмидта) и случайной диагонали со всеми положительными элементами; form .QADB=QDQ

    Полученная матрица может быть легко нормализована, чтобы иметь все единицы по диагонали: , где является диагональной матрицей с той же диагональю , как . Все три перечисленных выше способа генерации приводят к тому, что имеет недиагональные элементы, близкие к .BC=D1/2BD1/2D=diagBBBC0


Обновление: Старые темы

После публикации моего вопроса я обнаружил два почти дубликата в прошлом:

К сожалению, ни одна из этих тем не содержала удовлетворительного ответа (до сих пор :)


1
Вы можете создать случайную ортогональную матрицу с помощью процессов QR или Gram-Schmidt. Это будут "собственные векторы PCA". Добавьте масштаб к его столбцам (превратитесь в «нагрузки»). Получить ковариационную матрицу из этих нагрузок. Как-то так ...
ttnphns

1
Хм, хорошо. Представьте, что мы хотим создать nXkзагрузочную матрицу W, не полностью случайную, а ту, которую мы хотим (она WW'+diag(noise)определит матрицу cov, которую мы ищем. Единственная задача - исправить нормализованную по столбцам W (т.е. "собственные векторы"), чтобы стать ортогональными. Любой метод для декорреляции коррелированных переменных (здесь переменные являются собственными векторами), вероятно, подойдет. (Это
грубая

1
Ах, @whuber, теперь я понимаю, что ты имеешь в виду. Да, вы правы: если все недиагональные элементы идентичны и равны , то матрица действительно имеет полный ранг и положительно определена ... Это, конечно, не то, что я имела в виду: мне бы хотелось, чтобы распределение недиагональных элементов в каждой матрице, которые должны быть разумно «распределены», а не распределение по матрицам ...ρ
говорит амеба Reinstate Monica

3
Возможно, вы захотите взглянуть на дистрибутив
LKJ

2
@ttnphns: Мне кажется, я наконец понял, что ты был прав с самого начала: то, что ты предложил, - это самый простой способ достичь цели. Я добавил обновление в свой ответ, в основном реализовав то, что вы написали выше.
говорит амеба, восстанови Монику

Ответы:


14

Другие ответы придумывали хорошие приемы, чтобы решить мою проблему различными способами. Однако я нашел принципиальный подход, который, на мой взгляд, имеет большое преимущество, поскольку он концептуально очень ясен и прост в настройке.

В этой теме: Как эффективно генерировать случайные матрицы положительной-полуопределенной корреляции? - Я описал и предоставил код для двух эффективных алгоритмов генерации матриц случайной корреляции. Оба взяты из статьи Левандовски, Куровицки и Джо (2009), о которой @ssdecontrol упоминал в комментариях выше (большое спасибо!).

Пожалуйста, смотрите мой ответ там для большого количества цифр, объяснений и кода Matlab. Так называемый метод «виноградной лозы» позволяет генерировать матрицы случайной корреляции с любым распределением частичных корреляций и может использоваться для генерации матриц корреляции с большими недиагональными значениями. Вот пример рисунка из этой темы:

Винный метод

Единственное, что меняется между участками, - это один параметр, который контролирует, насколько распределение частичных корреляций сконцентрировано в районе .±1

Я также копирую свой код для генерации этих матриц, чтобы показать, что он не длиннее, чем другие методы, предложенные здесь. Пожалуйста, смотрите мой связанный ответ для некоторых объяснений. Значения betaparamдля рисунка выше были (а размерность была ).50,20,10,5,2,1d100

function S = vineBeta(d, betaparam)
    P = zeros(d);           %// storing partial correlations
    S = eye(d);

    for k = 1:d-1
        for i = k+1:d
            P(k,i) = betarnd(betaparam,betaparam); %// sampling from beta
            P(k,i) = (P(k,i)-0.5)*2;     %// linearly shifting to [-1, 1]
            p = P(k,i);
            for l = (k-1):-1:1 %// converting partial correlation to raw correlation
                p = p * sqrt((1-P(l,i)^2)*(1-P(l,k)^2)) + P(l,i)*P(l,k);
            end
            S(k,i) = p;
            S(i,k) = p;
        end
    end

    %// permuting the variables to make the distribution permutation-invariant
    permutation = randperm(d);
    S = S(permutation, permutation);
end

Обновление: собственные значения

@psarka спрашивает о собственных значениях этих матриц. На рисунке ниже я строю спектры собственных значений тех же шести корреляционных матриц, что и выше. Обратите внимание, что они постепенно уменьшаются; напротив, метод, предложенный @psarka, обычно приводит к корреляционной матрице с одним большим собственным значением, но все остальное довольно равномерно.

Собственные значения вышеприведенных матриц


Обновить. Действительно простой метод: несколько факторов

Подобно тому, что @ttnphns написал в комментариях выше и @GottfriedHelms в своем ответе, один очень простой способ достичь моей цели - случайным образом сгенерировать несколько ( ) факторных загрузок (случайная матрица из размера) , сформируйте ковариационную матрицу (которая, конечно, не будет полного ранга) и добавьте к ней случайную диагональную матрицу с положительными элементами, чтобы сделать полный ранг. Результирующая ковариационная матрица может быть нормализована, чтобы стать корреляционной матрицей (как описано в моем вопросе). Это очень просто и делает свое дело. Вот несколько примеров корреляционных матриц дляk<nWk×nWWDB=WW+Dk=100,50,20,10,5,1 :

матрицы случайной корреляции от случайных факторов

Единственным недостатком является то, что результирующая матрица будет иметь больших собственных значений, а затем внезапное падение, в отличие от хорошего затухания, показанного выше с помощью метода vine. Вот соответствующие спектры:k

Собственные спектры этих матриц

Вот код:

d = 100;    %// number of dimensions
k = 5;      %// number of factors

W = randn(d,k);
S = W*W' + diag(rand(1,d));
S = diag(1./sqrt(diag(S))) * S * diag(1./sqrt(diag(S)));

+1. Тем не менее, вот только напоминание к вашему последнему разделу о «метод фактора». Строго правильный подход Wподразумевает, что столбцы ортогональны (то есть косинусы между ними равны 0). WКонечно, просто генерировать случайные числа не дает этого. Если они не являются ортогональными - то есть факторы являются косыми (назовем тогда Wкак W_) - теорема о факторе не является, WW'но W_CW_'с тем, Cчтобы быть "корреляциями" (косинусами) между факторами. Теперь, C=Q'Qс Qбудучи неортогональную матрицу вращения вращения W_=inv(Q)'W(и так W=W_Q'). Сгенерируйте несколько Q- матрицу со столбцом ss = 1 и матрицей ss = размер матрицы.
ttnphns

... опечатка: нет W_=inv(Q)'W, конечно W_= W inv(Q)'.
ttnphns

WWW+DW

1
Переводя это на R:W = replicate(k, rnorm(d)); S = W%*%t(W) + diag(rnorm(d),nrow=d); S = diag(1/sqrt(diag(S)))%*%S%*%diag(1/sqrt(diag(S)))
Скотт Уорленд

1
@ Михай, хорошая мысль, и твои предложения, вероятно, самые простые. Вы также можете сделатьS <- matrix(nearPD(S, corr = TRUE, keepDiag = TRUE)$mat@x,ncol(S),ncol(S))
Скотт Уорленд

7

a

import numpy as np
from random import choice
import matplotlib.pyplot as plt

n = 100
a = 2

A = np.matrix([np.random.randn(n) + np.random.randn(1)*a for i in range(n)])
A = A*np.transpose(A)
D_half = np.diag(np.diag(A)**(-0.5))
C = D_half*A*D_half

vals = list(np.array(C.ravel())[0])
plt.hist(vals, range=(-1,1))
plt.show()
plt.imshow(C, interpolation=None)
plt.show()

Несколько равномерное распределение Результаты imshow


crsК[-a,a]Икс

Да, вы совершенно правы! (О, мальчик, это было действительно глупо: D). Я изменил случайную часть на randn (1) * a, и теперь она стала намного лучше.
psarka

К

aN

Недостатком этого метода является то, что результирующая матрица корреляции имеет одно большое собственное значение, но остальные почти одинаковы. Так что эта процедура не дает «общей» корреляционной матрицы ... Не то, чтобы я указал это в своем вопросе. Но @ssdecontrol упомянул в комментариях выше, что, очевидно, есть способы выборки из всех матриц корреляции; это выглядит интересно, но гораздо сложнее.
говорит амеба: восстанови Монику

6

Хм, после того как я сделал пример на своем языке MatMate, я вижу, что уже есть Python-ответ, который может быть предпочтительнее, потому что Python широко используется. Но поскольку у вас остались вопросы, я покажу вам свой подход с использованием языка Matmate-matrix-language, возможно, он более комментирует себя.

Способ 1
(с помощью MatMate):

v=12         // 12 variables
f=3          // subset-correlation based on 3 common factors
vg = v / f   // variables per subsets

 // generate hidden factor-matrix
             // randomu(rows,cols ,lowbound, ubound) gives uniform random matrix 
             //    without explicite bounds the default is: randomu(rows,cols,0,100)
L = {   randomu(vg,f)     || randomu(vg,f)/100  || randomu(vg,f)/100 , _
        randomu(vg,f)/100 || randomu(vg,f)      || randomu(vg,f)/100 , _
        randomu(vg,f)/100 || randomu(vg,f)/100  || randomu(vg,f)     }

 // make sure there is itemspecific variance
 // by appending a diagonal-matrix with random positive entries
L = L || mkdiag(randomu(v,1,10,20)) 
  // make covariance and correlation matrix
cov = L *'   // L multiplied  with its transpose
cor = covtocorr(cov)
                   set ccdezweite=3 ccfeldweite=8
                   list cor
cor = 
   1.000,   0.321,   0.919,   0.489,   0.025,   0.019,   0.019,   0.030,   0.025,   0.017,   0.014,   0.014
   0.321,   1.000,   0.540,   0.923,   0.016,   0.015,   0.012,   0.030,   0.033,   0.016,   0.012,   0.015
   0.919,   0.540,   1.000,   0.679,   0.018,   0.014,   0.012,   0.029,   0.028,   0.014,   0.012,   0.012
   0.489,   0.923,   0.679,   1.000,   0.025,   0.022,   0.020,   0.040,   0.031,   0.014,   0.011,   0.014
   0.025,   0.016,   0.018,   0.025,   1.000,   0.815,   0.909,   0.758,   0.038,   0.012,   0.018,   0.014
   0.019,   0.015,   0.014,   0.022,   0.815,   1.000,   0.943,   0.884,   0.035,   0.012,   0.014,   0.012
   0.019,   0.012,   0.012,   0.020,   0.909,   0.943,   1.000,   0.831,   0.036,   0.013,   0.015,   0.010
   0.030,   0.030,   0.029,   0.040,   0.758,   0.884,   0.831,   1.000,   0.041,   0.017,   0.022,   0.020
   0.025,   0.033,   0.028,   0.031,   0.038,   0.035,   0.036,   0.041,   1.000,   0.831,   0.868,   0.780
   0.017,   0.016,   0.014,   0.014,   0.012,   0.012,   0.013,   0.017,   0.831,   1.000,   0.876,   0.848
   0.014,   0.012,   0.012,   0.011,   0.018,   0.014,   0.015,   0.022,   0.868,   0.876,   1.000,   0.904
   0.014,   0.015,   0.012,   0.014,   0.014,   0.012,   0.010,   0.020,   0.780,   0.848,   0.904,   1.000

Проблема здесь может заключаться в том, что мы определяем блоки подматриц, которые имеют высокие корреляции внутри с небольшой корреляцией между ними, и это не программно, а с помощью постоянных выражений конкатенации. Может быть, этот подход может быть более элегантно смоделирован в python.


Метод 2 (а)
После этого, есть совершенно другой подход, где мы заполняем возможную оставшуюся ковариацию случайными величинами 100 процентов в матрицу факторных нагрузок. Это сделано в Pari / GP:

{L = matrix(8,8);  \\ generate an empty factor-loadings-matrix
for(r=1,8, 
   rv=1.0;    \\ remaining variance for variable is 1.0
   for(c=1,8,
        pv=if(c<8,random(100)/100.0,1.0); \\ define randomly part of remaining variance
        cv= pv * rv;  \\ compute current partial variance
        rv = rv - cv;     \\ compute the now remaining variance
        sg = (-1)^(random(100) % 2) ;  \\ also introduce randomly +- signs
        L[r,c] = sg*sqrt(cv) ;  \\ compute factor loading as signed sqrt of cv
       )
     );}

cor = L * L~

и полученная корреляционная матрица

     1.000  -0.7111  -0.08648   -0.7806   0.8394  -0.7674   0.6812    0.2765
   -0.7111    1.000   0.06073    0.7485  -0.7550   0.8052  -0.8273   0.05863
  -0.08648  0.06073     1.000    0.5146  -0.1614   0.1459  -0.4760  -0.01800
   -0.7806   0.7485    0.5146     1.000  -0.8274   0.7644  -0.9373  -0.06388
    0.8394  -0.7550   -0.1614   -0.8274    1.000  -0.5823   0.8065   -0.1929
   -0.7674   0.8052    0.1459    0.7644  -0.5823    1.000  -0.7261   -0.4822
    0.6812  -0.8273   -0.4760   -0.9373   0.8065  -0.7261    1.000   -0.1526
    0.2765  0.05863  -0.01800  -0.06388  -0.1929  -0.4822  -0.1526     1.000

Возможно, это генерирует корреляционную матрицу с доминирующими главными компонентами из-за кумулятивного правила генерации для матрицы факторных нагрузок. Также может быть лучше обеспечить положительную определенность, сделав последнюю часть дисперсии уникальным фактором. Я оставил это в программе, чтобы сосредоточиться на общем принципе.

Корреляционная матрица 100x100 имела следующие частоты корреляций (округленные до 1 разряда)

    e    f            e: entry(rounded) f: frequency
  -----------------------------------------------------
  -1.000, 108.000
  -0.900, 460.000
  -0.800, 582.000
  -0.700, 604.000
  -0.600, 548.000
  -0.500, 540.000
  -0.400, 506.000
  -0.300, 482.000
  -0.200, 488.000
  -0.100, 464.000
   0.000, 434.000
   0.100, 486.000
   0.200, 454.000
   0.300, 468.000
   0.400, 462.000
   0.500, 618.000
   0.600, 556.000
   0.700, 586.000
   0.800, 536.000
   0.900, 420.000
   1.000, 198.000

[Обновить]. Хм, матрица 100х100 плохо подготовлена; Pari / GP не может правильно определить собственные значения с помощью функции polroots (charpoly ()) даже с точностью до 200 цифр. Я выполнил вращение Якоби до pca-формы на матрице нагрузок L и нашел в основном чрезвычайно малые собственные значения, напечатал их в логарифмах до основания 10 (которое примерно соответствует положению десятичной точки). Читайте слева направо, а затем строка за строкой:

log_10(eigenvalues):
   1.684,   1.444,   1.029,   0.818,   0.455,   0.241,   0.117,  -0.423,  -0.664,  -1.040
  -1.647,  -1.799,  -1.959,  -2.298,  -2.729,  -3.059,  -3.497,  -3.833,  -4.014,  -4.467
  -4.992,  -5.396,  -5.511,  -6.366,  -6.615,  -6.834,  -7.535,  -8.138,  -8.263,  -8.766
  -9.082,  -9.482,  -9.940, -10.167, -10.566, -11.110, -11.434, -11.788, -12.079, -12.722
 -13.122, -13.322, -13.444, -13.933, -14.390, -14.614, -15.070, -15.334, -15.904, -16.278
 -16.396, -16.708, -17.022, -17.746, -18.090, -18.358, -18.617, -18.903, -19.186, -19.476
 -19.661, -19.764, -20.342, -20.648, -20.805, -20.922, -21.394, -21.740, -21.991, -22.291
 -22.792, -23.184, -23.680, -24.100, -24.222, -24.631, -24.979, -25.161, -25.282, -26.211
 -27.181, -27.626, -27.861, -28.054, -28.266, -28.369, -29.074, -29.329, -29.539, -29.689
 -30.216, -30.784, -31.269, -31.760, -32.218, -32.446, -32.785, -33.003, -33.448, -34.318

[обновление 2]
Метод 2 (b)
Улучшение может заключаться в увеличении дисперсии, специфичной для элементов, до некоторого не маргинального уровня и уменьшении до достаточно меньшего числа общих факторов (например, целочисленного квадрата от номера элемента):

{  dimr = 100;
   dimc = sqrtint(dimr);        \\ 10 common factors
   L = matrix(dimr,dimr+dimc);  \\ loadings matrix 
                                \\     with dimr itemspecific and 
                                \\          dimc common factors
   for(r=1,dim, 
         vr=1.0;                \\ complete variance per item 
         vu=0.05+random(100)/1000.0;   \\ random variance +0.05
                                       \\ for itemspecific variance
         L[r,r]=sqrt(vu);              \\ itemspecific factor loading  
         vr=vr-vu;
         for(c=1,dimc,
                cv=if(c<dimc,random(100)/100,1.0)*vr;
                vr=vr-cv;
                L[r,dimr+c]=(-1)^(random(100) % 2)*sqrt(cv)
             )
        );}

   cov=L*L~
   cp=charpoly(cov)   \\ does not work even with 200 digits precision
   pr=polroots(cp)    \\ spurious negative and complex eigenvalues...

Структура результата

по сроку распределения корреляций:образ

остается схожим (также неприятная неразложимость PariGP), но собственные значения, найденные с помощью вращения Якоби матрицы загрузки, теперь имеют лучшую структуру, для нового вычисленного примера я получил собственные значения как

log_10(eigenvalues):
   1.677,   1.326,   1.063,   0.754,   0.415,   0.116,  -0.262,  -0.516,  -0.587,  -0.783
  -0.835,  -0.844,  -0.851,  -0.854,  -0.858,  -0.862,  -0.862,  -0.868,  -0.872,  -0.873
  -0.878,  -0.882,  -0.884,  -0.890,  -0.895,  -0.896,  -0.896,  -0.898,  -0.902,  -0.904
  -0.904,  -0.909,  -0.911,  -0.914,  -0.920,  -0.923,  -0.925,  -0.927,  -0.931,  -0.935
  -0.939,  -0.939,  -0.943,  -0.948,  -0.951,  -0.955,  -0.956,  -0.960,  -0.967,  -0.969
  -0.973,  -0.981,  -0.986,  -0.989,  -0.997,  -1.003,  -1.005,  -1.011,  -1.014,  -1.019
  -1.022,  -1.024,  -1.031,  -1.038,  -1.040,  -1.048,  -1.051,  -1.061,  -1.064,  -1.068
  -1.070,  -1.074,  -1.092,  -1.092,  -1.108,  -1.113,  -1.120,  -1.134,  -1.139,  -1.147
  -1.150,  -1.155,  -1.158,  -1.166,  -1.171,  -1.175,  -1.184,  -1.184,  -1.192,  -1.196
  -1.200,  -1.220,  -1.237,  -1.245,  -1.252,  -1.262,  -1.269,  -1.282,  -1.287,  -1.290

Большое спасибо! Очень интересно, но мне потребуется некоторое время, чтобы переварить ...
говорит амеба Reinstate Monica

Мне все еще нужно внимательно изучить ваш ответ, но пока я читаю статью о выборке матриц случайной корреляции, и один из методов можно использовать для того, чтобы сделать именно то, что мне нужно. Я разместил ответ здесь, вам может быть интересно посмотреть! Это ссылка на гораздо более подробный ответ, который я написал в другой ветке.
говорит амеба, восстанови Монику

@amoeba: рад, что вы нашли что-то хорошее для вас! Это интересный вопрос, я сам вернусь к этому позже, возможно, улучшу / адаптирую процедуры MatMate (и сделаю их подпрограммами) в соответствии с работой, над которой вы работали.
Готфрид Хелмс

2

AВλA+(1-λ)Вλ

AВСλAA+λВВ+λССΣλзнак равно1λ0


AВ

Ах, но из такого алгоритма и подходящего разнообразия в «вершинах» (то есть матрицах), которые определяют ваш многогранник матриц положительно определенной корреляции, вы можете использовать выборочную выборку отклонения, чтобы получить любое распределение собственных значений, однородность записей, и т. д., что вы хотите. Тем не менее, мне не ясно, какой будет хорошая основа. Звучит как вопрос для кого-то, кто изучал абстрактную алгебру совсем недавно, чем я.
Эндрю М

Привет еще раз, я прочитал статью о выборке матриц случайной корреляции, и один из методов можно использовать именно для того, что мне нужно. Я разместил ответ здесь, вам может быть интересно посмотреть! Это ссылка на гораздо более подробный ответ, который я написал в другой ветке.
говорит амеба, восстанови Монику

2

R имеет пакет (clusterGeneration), который реализует метод в:

Пример:

> (cormat10 = clusterGeneration::rcorrmatrix(10, alphad = 1/100000000000000))
        [,1]   [,2]    [,3]     [,4]     [,5]   [,6]   [,7]    [,8]     [,9]   [,10]
 [1,]  1.000  0.344 -0.1406 -0.65786 -0.19411  0.246  0.688 -0.6146  0.36971 -0.1052
 [2,]  0.344  1.000 -0.4256 -0.35512  0.15973  0.192  0.340 -0.4907 -0.30539 -0.6104
 [3,] -0.141 -0.426  1.0000  0.01775 -0.61507 -0.485 -0.273  0.3492 -0.30284  0.1647
 [4,] -0.658 -0.355  0.0178  1.00000  0.00528 -0.335 -0.124  0.5256 -0.00583 -0.0737
 [5,] -0.194  0.160 -0.6151  0.00528  1.00000  0.273 -0.350 -0.0785  0.08285  0.0985
 [6,]  0.246  0.192 -0.4847 -0.33531  0.27342  1.000  0.278 -0.2220 -0.11010  0.0720
 [7,]  0.688  0.340 -0.2734 -0.12363 -0.34972  0.278  1.000 -0.6409  0.40314 -0.2800
 [8,] -0.615 -0.491  0.3492  0.52557 -0.07852 -0.222 -0.641  1.0000 -0.50796  0.1461
 [9,]  0.370 -0.305 -0.3028 -0.00583  0.08285 -0.110  0.403 -0.5080  1.00000  0.3219
[10,] -0.105 -0.610  0.1647 -0.07373  0.09847  0.072 -0.280  0.1461  0.32185  1.0000
> cormat10[lower.tri(cormat10)] %>% psych::describe()
   vars  n  mean   sd median trimmed mad   min  max range skew kurtosis   se
X1    1 45 -0.07 0.35  -0.08   -0.07 0.4 -0.66 0.69  1.35 0.03       -1 0.05

К сожалению, с этим не представляется возможным моделировать корреляции, которые следуют за распределением равномерного распределения. Кажется, корреляции становятся более сильными, если alphadзаданы очень малые значения, но даже при 1/100000000000000этом диапазон корреляций возрастет примерно до 1,40.

Тем не менее, я надеюсь, что это может быть кому-то полезно.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.