Более быстрая альтернатива R car::Anova для вычисления суммы квадратных матриц перекрестного произведения для подмножеств предикторов

Мне нужно вычислить сумму квадратов матрицы перекрестного произведения (на самом деле след этой матрицы) в многомерной линейной модели с Y (n x q) и X (n x p). Стандартный код R для этого:

require(MASS)
require(car)

# Example data 
q <- 10
n  <- 1000
p <- 10
Y <- mvrnorm(n, mu = rep(0, q), Sigma = diag(q))
X <- as.data.frame(mvrnorm(n, mu = rnorm(p), Sigma = diag(p)))

# Fit lm
fit <- lm( Y ~ ., data = X )

# Type I sums of squares
summary(manova(fit))$SS    

# Type III sums of squares
type = 3 # could be also 2 (II)
car::Anova(fit, type = type)$SSP

Это нужно делать тысячи раз, к сожалению, это становится медленным, когда количество предикторов относительно велико. Поскольку часто меня интересует только подмножество предикторов s, я попытался повторно реализовать этот расчет. Хотя моя реализация, напрямую переводящая линейную алгебру для s = 1 (ниже), работает быстрее для небольших размеров выборки (n),

# Hat matrix (X here stands for the actual design matrix)
H <- tcrossprod(tcrossprod(X, solve(crossprod(X))), X)

# Remove predictor of interest (e.g. 2)
X.r <- X[, -2]  
H1 <- tcrossprod(tcrossprod(X.r, solve(crossprod(X.r))), X.r) 

# Compute e.g. type III sum of squares
SS <- crossprod(Y, H - H1) %*% Y

car по-прежнему работает быстрее для больших n:

введите здесь описание изображения

Я уже пробовал реализацию Rcpp, и она добилась большого успеха, так как эти матричные продукты в R уже используют очень эффективный код.

Любая подсказка о том, как сделать это быстрее?

ОБНОВЛЕНИЕ

Прочитав ответы, я попробовал решение, предложенное в этом post, который использует факторизацию QR/SVD/Cholesky для вычисления матрицы шляпы. Однако кажется, что car::Anova все еще быстрее вычисляет все матрицы p = 30, чем я, вычисляющий только одну (s = 1)!! например n = 5000, q = 10:

Unit: milliseconds
 expr       min        lq      mean    median        uq       max neval
   ME 1137.5692 1202.9888 1257.8979 1251.6834 1318.9282 1398.9343    10
   QR 1005.9082 1031.9911 1084.5594 1037.5659 1095.7449 1364.9508    10
  SVD 1026.8815 1065.4629 1152.6631 1087.9585 1241.4977 1446.8318    10
 Chol  969.9089 1056.3093 1115.9608 1102.1169 1210.7782 1267.1274    10
  CAR  205.1665  211.8523  218.6195  214.6761  222.0973  242.4617    10

ОБНОВЛЕНИЕ 2

Лучшим решением на данный момент было просмотреть car::Anova код (т. е. функции car:::Anova.III.mlm и впоследствии car:::linearHypothesis.mlm) и повторно реализовать их для учета подмножества предикторов, а не всех.

Соответствующий код от car выглядит следующим образом (я пропустил проверки и немного упростил):

B <- coef(fit)                    # Model coefficients
M <- model.matrix(fit)            # Model matrix M
V <- solve(crossprod(M))          # M'M
p <- ncol(M)                      # Number of predictors in M
I.p <- diag(p)                    # Identity (p x p)
terms <- labels(terms(fit))       # terms (add intercept)       
terms <- c("(Intercept)", terms)   
n.terms <- length(terms)
assign <- fit$assign              # assignation terms <-> p variables
  
SSP <- as.list(rep(0, n.terms))   # Initialize empty list for sums of squares cross-product matrices
names(SSP) <- terms
  
for (term in 1:n.terms){
    subs <- which(assign == term - 1)
    L <- I.p[subs, , drop = FALSE]
    SSP[[term]] <- t(L %*% B) %*% solve(L %*% V %*% t(L)) %*% (L %*% B)
}

Тогда это просто вопрос выбора подмножества терминов.


person DGMartin    schedule 26.10.2020    source источник


Ответы (1)


Эту строку и аналогичную под ней для H1, вероятно, можно было бы улучшить:

H <- tcrossprod(tcrossprod(X, solve(crossprod(X))), X)

Общая идея заключается в том, что вам следует редко использовать solve(Y) %*% Z, потому что это то же самое, что и solve(Y, Z), но медленнее. Я не полностью расширил ваши вызовы tcrossprod, чтобы увидеть, какой будет наилучшая эквивалентная формулировка выражений для H и H1.

Вы также можете посмотреть на этот вопрос >https://stats.stackexchange.com/questions/139969/speeding-up-hat-matrices-like-xxx-1x-projection-matrices-and-other-as для описания выполнения этого с помощью QR разложение.

person user2554330    schedule 26.10.2020
comment
Спасибо! Я обновил вопрос, чтобы включить результаты через QR / SVD / Cholesky, однако машина, похоже, все еще быстрее. - person DGMartin; 26.10.2020
comment
Вы должны показать больше своего кода, чтобы у нас был воспроизводимый пример для работы. - person user2554330; 26.10.2020
comment
Я добавил некоторые примеры данных, они могут быть просто многомерными нормальными. На данный момент лучшее решение, которое я нашел, — это фактически заново реализовать интересующий меня фрагмент кода с помощью car, но с использованием только подмножества терминов. Это самый быстрый на данный момент. - person DGMartin; 27.10.2020
comment
Вы можете получить еще одно небольшое улучшение (около 7%), следуя предложению в моем ответе в последней строке цикла. - person user2554330; 27.10.2020
comment
И я не уверен, верно ли это вообще или только в примере, но L %*% V %*% t(L) — это скаляр, так что вы можете получить еще 7%, заменив эту последнюю строку на crossprod(L%*%B)/as.numeric(L %*% V %*% t(L)). - person user2554330; 27.10.2020
comment
Обычно это не так, иногда это матрица. Лучшее, что я получил, это то, что LB = L %*% B; crossprod(LB, solve(L %*% tcrossprod(V, L), LB)) достигает половины времени. - person DGMartin; 27.10.2020
comment
Хотя сейчас solve(crossprod(M)) действительно узкое место - person DGMartin; 27.10.2020