Distanze Mahalanobis a coppie


18

Devo calcolare la distanza di Mahalanobis del campione in R tra ogni coppia di osservazioni in una matrice di covariate. Ho bisogno di una soluzione efficiente, ovvero vengono calcolate solo distanze, e preferibilmente implementate in C / RCpp / Fortran ecc. Suppongo che , la matrice di covarianza della popolazione, sia sconosciuta e utilizzo il campione matrice di covarianza al suo posto.n×pn(n1)/2Σ

Sono particolarmente interessato a questa domanda poiché non sembra esserci un metodo di "consenso" per calcolare le distanze Mahalanobis a coppie in R, cioè non è implementato nella distfunzione né nella cluster::daisyfunzione. La mahalanobisfunzione non calcola le distanze a coppie senza lavoro aggiuntivo dal programmatore.

Questo è già stato chiesto qui distanza Mahalanobis a coppie in R , ma le soluzioni sembrano errate.

Ecco un metodo corretto ma terribilmente inefficiente (poiché vengono calcolate distanze):n×n

set.seed(0)
x0 <- MASS::mvrnorm(33,1:10,diag(c(seq(1,1/2,l=10)),10))
dM = as.dist(apply(x0, 1, function(i) mahalanobis(x0, i, cov = cov(x0))))

È abbastanza facile codificarmi in C, ma mi sembra che qualcosa di base dovrebbe avere una soluzione preesistente. Ce n'è uno?

Esistono altre soluzioni che mancano: HDMD::pairwise.mahalanobis()calcola distanze, quando sono richieste solo distanze uniche. sembra promettente, ma non voglio che la mia funzione provenga da un pacchetto che dipende , il che limita fortemente la capacità degli altri di eseguire il mio codice. A meno che questa implementazione non sia perfetta, preferirei scriverne una mia. Qualcuno ha esperienza con questa funzione?n×nn(n1)/2compositions::MahalanobisDist()rgl


Benvenuto. Puoi stampare le due matrici della distanza nella tua domanda? E cosa è "inefficiente" per te?
ttnphns,

1
Stai usando solo la matrice di covarianza del campione? Se è così, allora questo equivale a 1) centrando X; 2) calcolare l'SVD della X centrata, diciamo UDV '; 3) calcolare le distanze a coppie tra le file di U.
vqv

Grazie per aver pubblicato questo come domanda. Penso che la tua formula non sia corretta. Vedi la mia risposta qui sotto.
user603

@vqv Sì, matrice di covarianza di esempio. Il post originale viene modificato per riflettere questo.
Ah,

Vedi anche domanda molto simile stats.stackexchange.com/q/33518/3277 .
ttnphns,

Risposte:


21

A partire dalla soluzione "succint" di ahfoss, ho usato la decomposizione di Cholesky al posto dell'SVD.

cholMaha <- function(X) {
 dec <- chol( cov(X) )
 tmp <- forwardsolve(t(dec), t(X) )
 dist(t(tmp))
}

Dovrebbe essere più veloce, perché la risoluzione in avanti di un sistema triangolare è più veloce della densa moltiplicazione della matrice con la covarianza inversa ( vedi qui ). Ecco i benchmark con le soluzioni di ahfoss e whuber in diverse impostazioni:

 require(microbenchmark)
 set.seed(26565)
 N <- 100
 d <- 10

 X <- matrix(rnorm(N*d), N, d)

 A <- cholMaha( X = X ) 
 A1 <- fastPwMahal(x1 = X, invCovMat = solve(cov(X))) 
 sum(abs(A - A1)) 
 # [1] 5.973666e-12  Ressuring!

   microbenchmark(cholMaha(X),
                  fastPwMahal(x1 = X, invCovMat = solve(cov(X))),
                  mahal(x = X))
Unit: microseconds
expr          min       lq   median       uq      max neval
cholMaha    502.368 508.3750 512.3210 516.8960  542.806   100
fastPwMahal 634.439 640.7235 645.8575 651.3745 1469.112   100
mahal       839.772 850.4580 857.4405 871.0260 1856.032   100

 N <- 10
 d <- 5
 X <- matrix(rnorm(N*d), N, d)

   microbenchmark(cholMaha(X),
                  fastPwMahal(x1 = X, invCovMat = solve(cov(X))),
                  mahal(x = X)
                    )
Unit: microseconds
expr          min       lq    median       uq      max neval
cholMaha    112.235 116.9845 119.114 122.3970  169.924   100
fastPwMahal 195.415 201.5620 205.124 208.3365 1273.486   100
mahal       163.149 169.3650 172.927 175.9650  311.422   100

 N <- 500
 d <- 15
 X <- matrix(rnorm(N*d), N, d)

   microbenchmark(cholMaha(X),
                  fastPwMahal(x1 = X, invCovMat = solve(cov(X))),
                  mahal(x = X)
                    )
Unit: milliseconds
expr          min       lq     median       uq      max neval
cholMaha    14.58551 14.62484 14.74804 14.92414 41.70873   100
fastPwMahal 14.79692 14.91129 14.96545 15.19139 15.84825   100
mahal       12.65825 14.11171 39.43599 40.26598 41.77186   100

 N <- 500
 d <- 5
 X <- matrix(rnorm(N*d), N, d)

   microbenchmark(cholMaha(X),
                  fastPwMahal(x1 = X, invCovMat = solve(cov(X))),
                  mahal(x = X)
                    )
Unit: milliseconds
expr           min        lq      median        uq       max neval
cholMaha     5.007198  5.030110  5.115941  5.257862  6.031427   100
fastPwMahal  5.082696  5.143914  5.245919  5.457050  6.232565   100
mahal        10.312487 12.215657 37.094138 37.986501 40.153222   100

Quindi Cholesky sembra essere uniformemente più veloce.


3
+1 Ben fatto! Apprezzo la spiegazione del perché questa soluzione è più veloce.
whuber

In che modo maha (), ti dà la matrice distanza-coppia, invece della sola distanza di un punto?
sheß

1
Hai ragione, non è così, quindi la mia modifica non è del tutto rilevante. Lo eliminerò, ma forse un giorno aggiungerò una versione a coppie di maha () al pacchetto. Grazie per averlo segnalato.
Matteo Fasiolo,

1
Sarebbe carino! Non vedere l'ora di.
sheß

9

La formula standard per la distanza quadrata di Mahalanobis tra due punti dati è

D12=(X1-X2)TΣ-1(X1-X2)

dove è un vettore p × 1 corrispondente all'osservazione i . Tipicamente, la matrice di covarianza è stimata dai dati osservati. Senza contare inversione di matrice, questa operazione richiede p 2 + p moltiplicazioni e p 2 + 2 p aggiunte, ciascuno utilizzate n ( n - 1 ) / 2 volte.Xiop×1iop2+pp2+2pn(n-1)/2

Considera la seguente derivazione:

D12=(X1-X2)TΣ-1(X1-X2)=(X1-X2)TΣ-12Σ-12(X1-X2)=(X1TΣ-12-X2TΣ-12)(Σ-12X1-Σ-12X2)=(q1T-q2T)(q1-q2)

dove . Si noti chexTiΣ-1qio=Σ-12Xio. Ciò si basa sul fatto cheΣ-1XioTΣ-12=(Σ-12Xio)T=qioT è simmetrico, il che è dovuto al fatto che per qualsiasi matrice simmetrica diagonaleA=PEPT,Σ-12UN=PEPT

UN12T=(PE12PT)T=PTTE12TPT=PE12PT=UN12

Se lasciamo e notiamo che Σ - 1 è simmetrico, vediamo che Σ - 1UN=Σ-1Σ-1 deve anche essere simmetrico. SeXè lamatricen×pdelle osservazioni eQè lamatricen×ptale che larigaithdiQèqi, alloraQpuò essere brevemente espresso comeXΣ-1Σ-12Xn×pQn×piothQqioQ . Questo e i risultati precedenti lo implicanoXΣ-12

le uniche operazioni calcolate n ( n - 1 ) / 2 volte sono lemoltiplicazioni p e leaggiunte 2 p (al contrario dellemoltiplicazioni p 2 + p e p 2 + 2 p

DK=Σio=1p(QKio-Qio)2.
n(n-1)/2p2pp2+pp2+2paggiunte nel metodo sopra), risultante in un algoritmo di ordine computazionale di complessità invece dell'O originale ( p 2 n 2 ) .O(pn2+p2n)O(p2n2)
require(ICSNP) # for pair.diff(), C implementation

fastPwMahal = function(data) {

    # Calculate inverse square root matrix
    invCov = solve(cov(data))
    svds = svd(invCov)
    invCovSqr = svds$u %*% diag(sqrt(svds$d)) %*% t(svds$u)

    Q = data %*% invCovSqr

    # Calculate distances
    # pair.diff() calculates the n(n-1)/2 element-by-element
    # pairwise differences between each row of the input matrix
    sqrDiffs = pair.diff(Q)^2
    distVec = rowSums(sqrDiffs)

    # Create dist object without creating a n x n matrix
    attr(distVec, "Size") = nrow(data)
    attr(distVec, "Diag") = F
    attr(distVec, "Upper") = F
    class(distVec) = "dist"
    return(distVec)
}

Interessante. Siamo spiacenti, non lo so R. Puoi chiarire cosa pair.diff()fa e dare anche un esempio numerico con le stampe di ogni passaggio della tua funzione? Grazie.
ttnphns,

Ho modificato la risposta per includere la derivazione che giustifica questi calcoli, ma ho anche pubblicato una seconda risposta contenente un codice molto più conciso.
Ahfoss

7

Proviamo l'ovvio. A partire dal

Dij=(xixj)Σ1(xixj)=xiΣ1xi+xjΣ1xj2xiΣ1xj

ne consegue che possiamo calcolare il vettore

ui=xiΣ1xi

in tempo e matriceO(p2)

V=XΣ1X

nel tempo , molto probabilmente usando operazioni di array integrate (parallelizzabili) e quindi forma la soluzione comeO(pn2+p2n)

D=uu-2V

dove è il prodotto esterno rispetto a + : ( a b ) i j = a i + b j .+(un'B)ioj=un'io+Bj.

RUn'implementazione parallela succintamente la formulazione matematica (e assume, con essa, che in realtà è invertibile con inversa scritta h qui):Σ=Var(X)h

mahal <- function(x, h=solve(var(x))) {
  u <- apply(x, 1, function(y) y %*% h %*% y)
  d <- outer(u, u, `+`) - 2 * x %*% h %*% t(x)
  d[lower.tri(d)]
}

Si noti, per compatibilità con le altre soluzioni, che vengono restituiti solo gli elementi off-diagonali unici, anziché l'intera matrice di distanza quadrata (simmetrica, zero sulla diagonale). I grafici a dispersione mostrano che i suoi risultati concordano con quelli di fastPwMahal.

In C o C ++, RAM può essere riutilizzato e calcolata al volo, eliminando la necessità di stoccaggio intermedio di u u .uuuu

Studi di temporizzazione con che vanno da 33 a 5000 e p che vanno da 10 a 100 indicano che questa implementazione è da 1,5 a 5 volte più veloce rispetto a tale intervallo. Il miglioramento migliora con l' aumentare di p e n . Di conseguenza, possiamo aspettarci di essere superiori per i più piccoli p . Il pareggio si verifica intorno a p = 7 per n 100n335000p101001.55fastPwMahalpnfastPwMahalpp=7n100. Il fatto che gli stessi vantaggi computazionali di questa soluzione semplice riguardino altre implementazioni può essere una questione di come traggono vantaggio dalle operazioni di array vettorializzate.


Sembra buono. Suppongo che potrebbe essere reso ancora più rapido calcolando solo le diagonali inferiori, anche se non riesco a pensare fuori mano a un modo per farlo in R senza perdere le prestazioni veloci di applye outer... tranne per scoppiare Rcpp.
Ah,

applicare / esterno non ha alcun vantaggio in termini di velocità rispetto ai loop in semplice vaniglia.
user603

@ user603 Lo capisco in linea di principio, ma faccio i tempi. Inoltre, il punto principale dell'uso di questi costrutti è fornire un aiuto semantico per parallelizzare l'algoritmo: la differenza nel modo in cui lo esprimono è importante. (Potrebbe valere la pena ricordare che la domanda originale cerca implementazioni C / Fortran / ecc.) Ahfoss, ho pensato di limitare il calcolo anche al triangolo inferiore e sono d'accordo sul fatto che Rlì dentro non sembra esserci nulla da guadagnare.
whuber

5

dist()Xn×ppO(np)

S=XTX/n.

X

XL
LLLT=S-1SS-1O(np2+p3)

XX=UDVTX

S=VD2VT/n
S-1/2=VD-1VTn1/2.
XS-1/2=UVTn1/2
UnXO(np2)n>p

Ecco un'implementazione R del secondo metodo che non posso testare sull'iPad che sto usando per scrivere questa risposta.

u = svd(scale(x, center = TRUE, scale = FALSE), nv = 0)$u
dist(u)
# these distances need to be scaled by a factor of n

2

Questa è una soluzione molto più concisa. Si basa ancora sulla derivazione che coinvolge la matrice inversa di covarianza della radice quadrata (vedi la mia altra risposta a questa domanda), ma utilizza solo la base R e il pacchetto stats. Sembra essere leggermente più veloce (circa il 10% più veloce in alcuni benchmark che ho eseguito). Nota che restituisce la distanza di Mahalanobis, in contrapposizione alla distanza di Maha al quadrato.

fastPwMahal = function(x1,invCovMat) {
  SQRT = with(svd(invCovMat), u %*% diag(d^0.5) %*% t(v))
  dist(x1 %*% SQRT)
}

Questa funzione richiede una matrice di covarianza inversa e non restituisce un oggetto distanza - ma sospetto che questa versione ridotta della funzione sarà più generalmente utile per impilare gli utenti di scambio.


3
Ciò potrebbe essere migliorato sostituendolo SQRTcon la decomposizione di Cholesky chol(invCovMat).
vqv

1

n2

Se si utilizzano solo le funzionalità Fortran77 nell'interfaccia, la subroutine è ancora abbastanza portatile per gli altri.


1

C'è un modo molto semplice per farlo usando il pacchetto R "biotools". In questo caso otterrai una matrice Mahalanobis a distanza quadrata.

#Manly (2004, p.65-66)

x1 <- c(131.37, 132.37, 134.47, 135.50, 136.17)
x2 <- c(133.60, 132.70, 133.80, 132.30, 130.33)
x3 <- c(99.17, 99.07, 96.03, 94.53, 93.50)
x4 <- c(50.53, 50.23, 50.57, 51.97, 51.37)

#size (n x p) #Means 
x <- cbind(x1, x2, x3, x4) 

#size (p x p) #Variances and Covariances
Cov <- matrix(c(21.112,0.038,0.078,2.01, 0.038,23.486,5.2,2.844, 
        0.078,5.2,24.18,1.134, 2.01,2.844,1.134,10.154), 4, 4)

library(biotools)
Mahalanobis_Distance<-D2.dist(x, Cov)
print(Mahalanobis_Distance)

Puoi spiegarmi cosa significa una matrice a distanza quadrata? Rispettivamente: sono interessato alla distanza tra due punti / vettori, quindi cosa dice una matrice?
Ben

1

Questo è l'espansione con codice che la mia vecchia risposta ha spostato qui da un altro thread .

Ho fatto per molto tempo il calcolo di una matrice quadrata simmetrica di distanze Mahalanobis a coppie in SPSS tramite un approccio a matrice di cappelli usando la risoluzione di un sistema di equazioni lineari (poiché è più veloce dell'inversione della matrice di covarianza).

Non sono un utente R, quindi ho appena provato a riprodurre questa ricetta di @ahfoss qui in SPSS insieme alla "mia" ricetta, su un dato di 1000 casi per 400 variabili, e ho trovato la mia strada molto più veloce.


H

H(n-1)X(X'X)-1X'X

Quindi, centra le colonne della matrice dei dati, calcola la matrice del cappello, moltiplica per (n-1) ed esegui l'operazione opposta al doppio centraggio. Ottieni la matrice delle distanze quadrate di Mahalanobis.

hh2h1h2cos

Nelle nostre impostazioni, la matrice "double-centrate" è specificamente la matrice di cappello (moltiplicata per n-1), non prodotti scalari euclidei, e la risultante matrice di distanza quadrata è quindi la matrice di distanza quadrata di Mahalanobis, non la matrice di distanza euclidea quadrata.

HH(n-1)H= {H,H,...}Dmun'hun'l2=H+H'-2H(n-1)

Il codice in SPSS e sonda di velocità è sotto.


Questo primo codice corrisponde alla funzione @ahfoss fastPwMahaldella risposta citata . È equivalente ad esso matematicamente. Ma sto calcolando la matrice simmetrica completa delle distanze (tramite operazioni di matrice) mentre @ahfoss ha calcolato un triangolo della matrice simmetrica (elemento per elemento).

matrix. /*Matrix session in SPSS;
        /*note: * operator means matrix multiplication, &* means usual, elementwise multiplication.
get data. /*Dataset 1000 cases x 400 variables
!cov(data%cov). /*compute usual covariances between variables [this is my own matrix function].
comp icov= inv(cov). /*invert it
call svd(icov,u,s,v). /*svd
comp isqrcov= u*sqrt(s)*t(v). /*COV^(-1/2)
comp Q= data*isqrcov. /*Matrix Q (see ahfoss answer)
!seuclid(Q%m). /*Compute 1000x1000 matrix of squared euclidean distances;
               /*computed here from Q "data" they are the squared Mahalanobis distances.
/*print m. /*Done, print
end matrix.

Time elapsed: 3.25 sec

Di seguito è la mia modifica per renderlo più veloce:

matrix.
get data.
!cov(data%cov).
/*comp icov= inv(cov). /*Don't invert.
call eigen(cov,v,s2). /*Do sdv or eigen decomposition (eigen is faster),
/*comp isqrcov= v * mdiag(1/sqrt(s2)) * t(v). /*compute 1/sqrt of the eigenvalues, and compose the matrix back, so we have COV^(-1/2).
comp isqrcov= v &* (make(nrow(cov),1,1) * t(1/sqrt(s2))) * t(v). /*Or this way not doing matrix multiplication on a diagonal matrix: a bit faster .
comp Q= data*isqrcov.
!seuclid(Q%m).
/*print m.
end matrix.

Time elapsed: 2.40 sec

X(X'X)-1X'(X'X)-1X'solve(X'X,X')

matrix.
get data.
!center(data%data). /*Center variables (columns).
comp hat= data*solve(sscp(data),t(data))*(nrow(data)-1). /*hat matrix, and multiply it by n-1 (i.e. by df of covariances).
comp ss= diag(hat)*make(1,ncol(hat),1). /*Now using its diagonal, the leverages (as column propagated into matrix).
comp m= ss+t(ss)-2*hat. /*compute matrix of squared Mahalanobis distances via "cosine rule".
/*print m.
end matrix.

[Notice that if in "comp ss" and "comp m" lines you use "sscp(t(data))",
 that is, DATA*t(DATA), in place of "hat", you get usual sq. 
 euclidean distances]

Time elapsed: 0.95 sec

0

La formula che hai pubblicato non sta calcolando ciò che pensi di essere in elaborazione (una statistica U).

Nel codice che ho pubblicato, utilizzo cov(x1)come matrice di ridimensionamento (questa è la varianza delle differenze a coppie dei dati). Stai utilizzando cov(x0)(questa è la matrice di covarianza dei tuoi dati originali). Penso che questo sia un errore da parte tua. Il punto centrale dell'uso delle differenze a coppie è che ti solleva dal presupposto che la distribuzione multivariata dei tuoi dati sia simmetrica attorno a un centro di simmetria (o di dover stimare quel centro di simmetria per quella materia, poiché crossprod(x1)è proporzionale a cov(x1)). Ovviamente, usando cov(x0)te lo perdi.

Questo è ben spiegato nel documento a cui ho collegato la mia risposta originale.


1
Penso che stiamo parlando di due cose diverse qui. Il mio metodo calcola la distanza di Mahalanobis, che ho verificato rispetto ad alcune altre formule. Anche la mia formula è stata ora verificata in modo indipendente da Matteo Fasioloe (presumo) whuberin questo thread. Il tuo è diverso. Sarei interessato a capire cosa stai calcolando, ma è chiaramente diverso dalla distanza di Mahalanobis come generalmente definita.
Ah,

@ahfoss: 1) mahalanobis è la distanza della X da un punto di simmetria nella loro metrica. Nel tuo caso, le X sono una matrice * (n-1) / 2 od differenze a coppie, il loro centro di simmetria è il vettore 0_p e la loro metrica è ciò che ho chiamato cov (X1) nel mio codice. 2) chiediti perché usi una statistica U in primo luogo, e come spiega il documento vedrai che l'uso di cov (x0) sconfigge tale scopo.
user603

XXOp

cov(x0)SsolSτLQD
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.