Regresión lineal simple por pares rápida entre variables en un marco de datos
performance regression (1)
Algunos resultados estadísticos / antecedentes
(Enlace en la imagen: Función para calcular R2 (R-cuadrado) en R )
Detalles computacionales
Los cálculos involucrados aquí son básicamente el cálculo de la matriz de varianza-covarianza. Una vez que lo tenemos, los resultados para todas las regresión por pares son solo aritmética matricial de elementos.
La matriz de varianza-covarianza puede obtenerse mediante la función R
cov
, pero las funciones que se encuentran a continuación la
calculan manualmente utilizando el
crossprod
.
La ventaja es que, obviamente, puede beneficiarse de una biblioteca BLAS optimizada si la tiene.
Tenga en cuenta que una cantidad significativa de simplificación se realiza de esta manera.
La función
cov
R tiene
use
argumentos que permite el manejo de
NA
, pero no lo hace
crossprod
.
¡Estoy asumiendo que sus datos no tienen valores faltantes en absoluto!
Si tiene valores faltantes, elimínelos usted mismo con
na.omit(dat)
.
La inicial
as.matrix
que convierte un marco de datos en una matriz puede ser una sobrecarga.
En principio, si codifico todo en C / C ++, puedo eliminar esta coacción.
Y, de hecho, muchos aritméticos matriciales de matriz de elementos pueden fusionarse en un único nido de bucle.
Sin embargo, realmente me molesto en hacer esto en este momento (ya que no tengo tiempo).
Algunas personas pueden argumentar que el formato de la declaración final es inconveniente. Podría haber otro formato:
- una lista de marcos de datos, cada uno de los cuales proporciona el resultado de la regresión para una variable LHS particular;
- una lista de marcos de datos, cada uno de los cuales da el resultado de la regresión para una variable RHS en particular.
Esto es realmente basado en la opinión.
De todos modos, siempre puede hacer un
split.data.frame
por la columna "LHS" o la columna "RHS" en el marco de datos que le devuelvo.
Función R
pairwise_simpleLM
pairwise_simpleLM <- function (dat) {
## matrix and its dimension (n: numbeta.ser of data; p: numbeta.ser of variables)
dat <- as.matrix(dat)
n <- nrow(dat)
p <- ncol(dat)
## variable summary: mean, (unscaled) covariance and (unscaled) variance
m <- colMeans(dat)
V <- crossprod(dat) - tcrossprod(m * sqrt(n))
d <- diag(V)
## R-squared (explained variance) and its complement
R2 <- (V ^ 2) * tcrossprod(1 / d)
R2_complement <- 1 - R2
R2_complement[seq.int(from = 1, by = p + 1, length = p)] <- 0
## slope and intercept
beta <- V * rep(1 / d, each = p)
alpha <- m - beta * rep(m, each = p)
## residual sum of squares and standard error
RSS <- R2_complement * d
sig <- sqrt(RSS * (1 / (n - 2)))
## statistics for slope
beta.se <- sig * rep(1 / sqrt(d), each = p)
beta.tv <- beta / beta.se
beta.pv <- 2 * pt(abs(beta.tv), n - 2, lower.tail = FALSE)
## F-statistic and p-value
F.fv <- (n - 2) * R2 / R2_complement
F.pv <- pf(F.fv, 1, n - 2, lower.tail = FALSE)
## export
data.frame(LHS = rep(colnames(dat), times = p),
RHS = rep(colnames(dat), each = p),
alpha = c(alpha),
beta = c(beta),
beta.se = c(beta.se),
beta.tv = c(beta.tv),
beta.pv = c(beta.pv),
sig = c(sig),
R2 = c(R2),
F.fv = c(F.fv),
F.pv = c(F.pv),
stringsAsFactors = FALSE)
}
Vamos a comparar el resultado en el conjunto de datos de juguete en la pregunta.
oo <- poor(dat)
rr <- pairwise_simpleLM(dat)
all.equal(oo, rr)
#[1] TRUE
Veamos su salida:
rr[1:3, ]
# LHS RHS alpha beta beta.se beta.tv beta.pv sig
#1 A A 0.00000000 1.0000000 0.00000000 Inf 0.000000e+00 0.0000000
#2 B A 0.05550367 0.6206434 0.04456744 13.92594 5.796437e-25 0.1252402
#3 C A 0.05809455 1.2215173 0.04790027 25.50126 4.731618e-45 0.1346059
# R2 F.fv F.pv
#1 1.0000000 Inf 0.000000e+00
#2 0.6643051 193.9317 5.796437e-25
#3 0.8690390 650.3142 4.731618e-45
Cuando tenemos el mismo LHS y RHS, la regresión no tiene sentido, por lo tanto, la intersección es 0, la pendiente es 1, etc.
¿Qué pasa con la velocidad? Todavía usando este ejemplo de juguete:
library(microbenchmark)
microbenchmark("poor_man''s" = poor(dat), "fast" = pairwise_simpleLM(dat))
#Unit: milliseconds
# expr min lq mean median uq max
# poor_man''s 127.270928 129.060515 137.813875 133.390722 139.029912 216.24995
# fast 2.732184 3.025217 3.381613 3.134832 3.313079 10.48108
La brecha se va haciendo cada vez más amplia ya que tenemos más variables. Por ejemplo, con 10 variables tenemos:
set.seed(0)
X <- matrix(runif(100), 100, 10, dimnames = list(1:100, LETTERS[1:10]))
b <- runif(10)
DAT <- X * b[col(X)] + matrix(rnorm(100 * 10, 0, 0.1), 100, 10)
DAT <- as.data.frame(DAT)
microbenchmark("poor_man''s" = poor(DAT), "fast" = pairwise_simpleLM(DAT))
#Unit: milliseconds
# expr min lq mean median uq max
# poor_man''s 548.949161 551.746631 573.009665 556.307448 564.28355 801.645501
# fast 3.365772 3.578448 3.721131 3.621229 3.77749 6.791786
Función R
general_paired_simpleLM
general_paired_simpleLM <- function (dat_LHS, dat_RHS) {
## matrix and its dimension (n: numbeta.ser of data; p: numbeta.ser of variables)
dat_LHS <- as.matrix(dat_LHS)
dat_RHS <- as.matrix(dat_RHS)
if (nrow(dat_LHS) != nrow(dat_RHS)) stop("''dat_LHS'' and ''dat_RHS'' don''t have same number of rows!")
n <- nrow(dat_LHS)
pl <- ncol(dat_LHS)
pr <- ncol(dat_RHS)
## variable summary: mean, (unscaled) covariance and (unscaled) variance
ml <- colMeans(dat_LHS)
mr <- colMeans(dat_RHS)
vl <- colSums(dat_LHS ^ 2) - ml * ml * n
vr <- colSums(dat_RHS ^ 2) - mr * mr * n
##V <- crossprod(dat - rep(m, each = n)) ## cov(u, v) = E[(u - E[u])(v - E[v])]
V <- crossprod(dat_LHS, dat_RHS) - tcrossprod(ml * sqrt(n), mr * sqrt(n)) ## cov(u, v) = E[uv] - E{u]E[v]
## R-squared (explained variance) and its complement
R2 <- (V ^ 2) * tcrossprod(1 / vl, 1 / vr)
R2_complement <- 1 - R2
## slope and intercept
beta <- V * rep(1 / vr, each = pl)
alpha <- ml - beta * rep(mr, each = pl)
## residual sum of squares and standard error
RSS <- R2_complement * vl
sig <- sqrt(RSS * (1 / (n - 2)))
## statistics for slope
beta.se <- sig * rep(1 / sqrt(vr), each = pl)
beta.tv <- beta / beta.se
beta.pv <- 2 * pt(abs(beta.tv), n - 2, lower.tail = FALSE)
## F-statistic and p-value
F.fv <- (n - 2) * R2 / R2_complement
F.pv <- pf(F.fv, 1, n - 2, lower.tail = FALSE)
## export
data.frame(LHS = rep(colnames(dat_LHS), times = pr),
RHS = rep(colnames(dat_RHS), each = pl),
alpha = c(alpha),
beta = c(beta),
beta.se = c(beta.se),
beta.tv = c(beta.tv),
beta.pv = c(beta.pv),
sig = c(sig),
R2 = c(R2),
F.fv = c(F.fv),
F.pv = c(F.pv),
stringsAsFactors = FALSE)
}
Aplique esto al Ejemplo 1 en la pregunta.
general_paired_simpleLM(dat[1:3], dat[4:5])
# LHS RHS alpha beta beta.se beta.tv beta.pv sig
#1 A D -0.009212582 0.3450939 0.01171768 29.45071 1.772671e-50 0.09044509
#2 B D 0.012474593 0.2389177 0.01420516 16.81908 1.201421e-30 0.10964516
#3 C D -0.005958236 0.4565443 0.01397619 32.66585 1.749650e-54 0.10787785
#4 A E 0.008650812 -0.4798639 0.01963404 -24.44040 1.738263e-43 0.10656866
#5 B E 0.012738403 -0.3437776 0.01949488 -17.63426 3.636655e-32 0.10581331
#6 C E 0.009068106 -0.6430553 0.02183128 -29.45569 1.746439e-50 0.11849472
# R2 F.fv F.pv
#1 0.8984818 867.3441 1.772671e-50
#2 0.7427021 282.8815 1.201421e-30
#3 0.9158840 1067.0579 1.749650e-54
#4 0.8590604 597.3333 1.738263e-43
#5 0.7603718 310.9670 3.636655e-32
#6 0.8985126 867.6375 1.746439e-50
Aplique esto al Ejemplo 2 en la pregunta.
general_paired_simpleLM(dat[1:4], dat[5])
# LHS RHS alpha beta beta.se beta.tv beta.pv sig
#1 A E 0.008650812 -0.4798639 0.01963404 -24.44040 1.738263e-43 0.1065687
#2 B E 0.012738403 -0.3437776 0.01949488 -17.63426 3.636655e-32 0.1058133
#3 C E 0.009068106 -0.6430553 0.02183128 -29.45569 1.746439e-50 0.1184947
#4 D E 0.066190196 -1.3767586 0.03597657 -38.26820 9.828853e-61 0.1952718
# R2 F.fv F.pv
#1 0.8590604 597.3333 1.738263e-43
#2 0.7603718 310.9670 3.636655e-32
#3 0.8985126 867.6375 1.746439e-50
#4 0.9372782 1464.4551 9.828853e-61
Aplique esto al Ejemplo 3 en la pregunta.
general_paired_simpleLM(dat[1], dat[2:5])
# LHS RHS alpha beta beta.se beta.tv beta.pv sig
#1 A B 0.112229318 1.0703491 0.07686011 13.92594 5.796437e-25 0.16446951
#2 A C 0.025628210 0.7114422 0.02789832 25.50126 4.731618e-45 0.10272687
#3 A D -0.009212582 0.3450939 0.01171768 29.45071 1.772671e-50 0.09044509
#4 A E 0.008650812 -0.4798639 0.01963404 -24.44040 1.738263e-43 0.10656866
# R2 F.fv F.pv
#1 0.6643051 193.9317 5.796437e-25
#2 0.8690390 650.3142 4.731618e-45
#3 0.8984818 867.3441 1.772671e-50
#4 0.8590604 597.3333 1.738263e-43
Incluso podemos simplemente hacer una regresión lineal simple entre dos variables:
general_paired_simpleLM(dat[1], dat[2])
# LHS RHS alpha beta beta.se beta.tv beta.pv sig
#1 A B 0.1122293 1.070349 0.07686011 13.92594 5.796437e-25 0.1644695
# R2 F.fv F.pv
#1 0.6643051 193.9317 5.796437e-25
Esto significa que la función
simpleLM
ahora está obsoleta.
Apéndice: Markdown (necesita compatibilidad con MathJax) para la imagen
Denote our variables by $x_1$, $x_2$, etc, a pairwise simple linear regression takes the form $$x_i = /alpha_{ij} + /beta_{ij}x_j$$ where $/alpha_{ij}$ and $/beta_{ij}$ is the intercept and the slope of $x_i /sim x_j$, respectively. We also denote $m_i$ and $v_i$ as the sample mean and **unscaled** sample variance of $x_i$. Here, the unscaled variance is just the sum of squares without dividing by sample size, that is $v_i = /sum_{k = 1}^n(x_{ik} - m_i)^2 = (/sum_{k = 1}^nx_{ik}^2) - n m_i^2$. We also denote $V_{ij}$ as the **unscaled** covariance between $x_i$ and $x_j$: $V_{ij} = /sum_{k = 1}^n(x_{ik} - m_i)(x_{jk} - m_j)$ = $(/sum_{k = 1}^nx_{ik}x_{jk}) - nm_im_j$.
Using the results for a simple linear regression given in [Function to calculate R2 (R-squared) in R](https://stackoverflow.com/a/40901487/4891738), we have $$/beta_{ij} = V_{ij} / / / v_j,/quad /alpha_{ij} = m_i - /beta_{ij}m_j,/quad r_{ij}^2 = V_{ij}^2 / / / (v_iv_j),$$ where $r_{ij}^2$ is the R-squared. Knowing $r_{ij}^2 = RSS_{ij} / / / TSS_{ij}$ where $RSS_{ij}$ and $TSS_{ij} = v_i$ are residual sum of squares and total sum of squares of $x_i /sim x_j$, we can derive $RSS_{ij}$ and residual standard error $/sigma_{ij}$ **without actually computing residuals**: $$RSS_{ij} = (1 - r_{ij}^2)v_i,/quad /sigma_{ij} = /sqrt{RSS_{ij} / / / (n - 2)}.$$
F-statistic $F_{ij}$ and associated p-value $p_{ij}^F$ can also be obtained from sum of squares: $$F_{ij} = /tfrac{(TSS_{ij} - RSS_{ij}) / / / 1}{RSS_{ij} / / / (n - 2)} = (n - 2) r_{ij}^2 / / / (1 - r_{ij}^2),/quad p_{ij}^F = 1 - /texttt{CDF_F}(F_{ij};/ 1,/ n - 2),$$ where $/texttt{CDF_F}$ denotes the CDF of F-distribution.
The only thing left is the standard error $e_{ij}$, t-statistic $t_{ij}$ and associated p-value $p_{ij}^t$ for $/beta_{ij}$, which are $$e_{ij} = /sigma_{ij} / / / /sqrt{v_i},/quad t_{ij} = /beta_{ij} / / / e_{ij},/quad p_{ij}^t = 2 * /texttt{CDF_t}(-|t_{ij}|; / n - 2),$$ where $/texttt{CDF_t}$ denotes the CDF of t-distribution.
He visto muchas veces la regresión lineal simple por parejas o general en el Desbordamiento de pila. Aquí hay un conjunto de datos de juguete para este tipo de problema.
set.seed(0)
X <- matrix(runif(100), 100, 5, dimnames = list(1:100, LETTERS[1:5]))
b <- c(1, 0.7, 1.3, 2.9, -2)
dat <- X * b[col(X)] + matrix(rnorm(100 * 5, 0, 0.1), 100, 5)
dat <- as.data.frame(dat)
pairs(dat)
Así que básicamente queremos calcular 5 * 4 = 20 líneas de regresión:
----- A ~ B A ~ C A ~ D A ~ E
B ~ A ----- B ~ C B ~ D B ~ E
C ~ A C ~ B ----- C ~ D C ~ E
D ~ A D ~ B D ~ C ----- D ~ E
E ~ A E ~ B E ~ C E ~ D -----
Aquí está la estrategia de un hombre pobre :
poor <- function (dat) {
n <- nrow(dat)
p <- ncol(dat)
## all formulae
LHS <- rep(colnames(dat), p)
RHS <- rep(colnames(dat), each = p)
## function to fit and summarize a single model
fitmodel <- function (LHS, RHS) {
if (RHS == LHS) {
z <- data.frame("LHS" = LHS, "RHS" = RHS,
"alpha" = 0,
"beta" = 1,
"beta.se" = 0,
"beta.tv" = Inf,
"beta.pv" = 0,
"sig" = 0,
"R2" = 1,
"F.fv" = Inf,
"F.pv" = 0,
stringsAsFactors = FALSE)
} else {
result <- summary(lm(reformulate(RHS, LHS), data = dat))
z <- data.frame("LHS" = LHS, "RHS" = RHS,
"alpha" = result$coefficients[1, 1],
"beta" = result$coefficients[2, 1],
"beta.se" = result$coefficients[2, 2],
"beta.tv" = result$coefficients[2, 3],
"beta.pv" = result$coefficients[2, 4],
"sig" = result$sigma,
"R2" = result$r.squared,
"F.fv" = result$fstatistic[[1]],
"F.pv" = pf(result$fstatistic[[1]], 1, n - 2, lower.tail = FALSE),
stringsAsFactors = FALSE)
}
z
}
## loop through all models
do.call("rbind.data.frame", c(Map(fitmodel, LHS, RHS),
list(make.row.names = FALSE,
stringsAsFactors = FALSE)))
}
La lógica es clara: obtener todos los pares, construir la fórmula del modelo (
reformulate
), ajustar una regresión (
lm
), hacer un resumen
summary
, devolver todas las estadísticas y
rbind
para que sean un marco de datos.
OK, bien, pero ¿y si hay
p
variables?
¡Entonces tenemos que hacer
p * (p - 1)
regresiones!
Una mejora inmediata que se me ocurre, es adaptar un modelo lineal con múltiples LHS . Por ejemplo, la primera columna de esa matriz de fórmula se fusiona para
cbind(B, C, D, E) ~ A
Esto reduce el número de regresión de
p * (p - 1)
a
p
.
Pero definitivamente podemos hacerlo aún mejor sin utilizar
lm
y
summary
.
Aquí está mi intento anterior:
¿Existe una estimación rápida de regresión simple (una línea de regresión con solo intercepción y pendiente)?
.
Es rápido porque usa la covarianza entre variables para la estimación, como resolver la
ecuación normal
.
Pero la función
simpleLM
allí es bastante limitada:
- necesita calcular vectores residuales para estimar el error estándar residual, que es un cuello de botella en el rendimiento;
-
no es compatible con múltiples LHS, por lo que debe llamarse
p * (p - 1)
veces en la configuración de regresión por pares).
¿Podemos generalizarlo para una rápida regresión de pares, escribiendo una función
pairwise_simpleLM
?
Regresión lineal simple apareada general
Una variación más útil de la regresión de pares anterior es la regresión general emparejada entre un conjunto de variables de LHS y un conjunto de variables de RHS.
Ejemplo 1
Ajuste la regresión apareada entre las variables LHS
A
,
B
,
C
y las variables RHS
D
,
E
, es decir, ajuste 6 líneas de regresión lineal simple:
A ~ D A ~ E
B ~ D B ~ E
C ~ D C ~ E
Ejemplo 2
Ajuste una regresión lineal simple con múltiples variables de LHS a una variable de RHS en particular, por ejemplo:
cbind(A, B, C, D) ~ E
Ejemplo 3
Ajuste una regresión lineal simple con una variable de LHS particular y un conjunto de variables de RHS de una en una, por ejemplo:
A ~ B A ~ C A ~ D A ~ E
¿Podemos también tener una función rápida
general_paired_simpleLM
para esto?
Precaución
- Todas las variables deben ser numéricas; los factores no están permitidos o la regresión por pares no tiene sentido.
- La regresión ponderada no se discute, ya que el método de varianza-covarianza no está justificado en ese caso.