Szybka regresja liniowa według grup

Mam 500K użytkowników i muszę obliczyć regresję liniową (z przechwyceniem) dla każdego z nich.

Każdy użytkownik ma około 30 rekordów.

Próbowałem z dplyr i lm i to jest zbyt wolne. Około 2 SEK przez użytkownika.

  df%>%                       
      group_by(user_id, add =  FALSE) %>%
      do(lm = lm(Y ~ x, data = .)) %>%
      mutate(lm_b0 = summary(lm)$coeff[1],
             lm_b1 = summary(lm)$coeff[2]) %>%
      select(user_id, lm_b0, lm_b1) %>%
      ungroup()
    )

Próbowałem użyć lm.fit, który jest znany jako szybszy, ale nie wydaje się być kompatybilny z dplyr.

Czy istnieje szybki sposób na regresję liniową według grupy?

 16
Author: BrodieG, 2015-04-22

4 answers

Możesz użyć podstawowych formuł do obliczania nachylenia i regresji. Robi wiele niepotrzebnych rzeczy, jeśli zależy Ci tylko na tych dwóch liczbach. Tutaj używam data.table do agregacji, ale można to zrobić również w bazie R (lub dplyr):

system.time(
  res <- DT[, 
    {
      ux <- mean(x)
      uy <- mean(y)
      slope <- sum((x - ux) * (y - uy)) / sum((x - ux) ^ 2)
      list(slope=slope, intercept=uy - slope * ux)
    }, by=user.id
  ]
)

Produkuje dla 500K użytkowników ~ 30 obs każdy (w sekundach):

 user  system elapsed 
 7.35    0.00    7.36 

Lub około15 mikrosekund na użytkownika . I aby potwierdzić, że działa to zgodnie z oczekiwaniami:

> summary(DT[user.id==89663, lm(y ~ x)])$coefficients
             Estimate Std. Error   t value  Pr(>|t|)
(Intercept) 0.1965844  0.2927617 0.6714826 0.5065868
x           0.2021210  0.5429594 0.3722580 0.7120808
> res[user.id == 89663]
   user.id    slope intercept
1:   89663 0.202121 0.1965844

Dane:

set.seed(1)
users <- 5e5
records <- 30
x <- runif(users * records)
DT <- data.table(
  x=x, y=x + runif(users * records) * 4 - 2, 
  user.id=sample(users, users * records, replace=T)
)
 18
Author: BrodieG,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-04-22 21:28:58

Jeśli chcesz tylko współczynników, użyłbym user_id jako czynnika regresji. Korzystanie z symulowanego kodu danych @ miles2know (choć zmiana nazwy od obiektu innego niż exp() dzielącego tę nazwę wygląda dziwnie)

dat <- data.frame(id = rep(c("a","b","c"), each = 20),
                  x = rnorm(60,5,1.5),
                  y = rnorm(60,2,.2))

mod = lm(y ~ x:id + id + 0, data = dat)

Nie pasujemy do globalnego przechwytywania (+ 0) tak, że przechwytywanie dla każdego id jest współczynnikiem id, a nie x samo w sobie, tak, że interakcje x:id są nachyleniem dla każdego id:

coef(mod)
#      ida      idb      idc    x:ida    x:idb    x:idc 
# 1.779686 1.893582 1.946069 0.039625 0.033318 0.000353 

Więc dla a poziomu id współczynnik ida , 1.78, jest przechył i x:ida współczynnik, 0.0396, jest nachylenie.

Zostawię zebranie tych współczynników w odpowiednich kolumnach ramki danych dla Ciebie...

To rozwiązanie powinno być bardzo szybkie, ponieważ nie musisz zajmować się podzbiorami ramek danych. Może być jeszcze bardziej przyspieszony {[13] } lub tym podobne.

Uwaga na skalowalność:

Właśnie wypróbowałem to na symulowanych danych @nrussell i uruchomiłem alokację pamięci problemy. W zależności od ilości posiadanej pamięci może nie działać za jednym zamachem, ale prawdopodobnie można to zrobić w partiach identyfikatorów użytkowników. Jakaś kombinacja jego odpowiedzi i mojej odpowiedzi może być najszybsza ogólnie - - - lub nrussell może być po prostu szybsza - - - rozszerzenie współczynnika ID użytkownika na tysiące fałszywych zmiennych może nie być wydajne obliczeniowo, jak czekałem więcej niż kilka minut teraz na uruchomienie na zaledwie 5000 identyfikatorów użytkowników.

 11
Author: Gregor,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-04-22 18:42:53

Aktualizacja: Jak zauważył Dirk, moje oryginalne podejście może zostać znacznie ulepszone poprzez bezpośrednie określenie x i Y, zamiast używać interfejsu fastLm opartego na formułach, który wiąże się (dość znaczący) z kosztami przetwarzania. Dla porównania, używając oryginalnego pełnego zestawu danych,

R> system.time({
  dt[,c("lm_b0", "lm_b1") := as.list(
    unname(fastLm(x, Y)$coefficients))
    ,by = "user_id"]
})
#  user  system elapsed 
#55.364   0.014  55.401 
##
R> system.time({
  dt[,c("lm_b0","lm_b1") := as.list(
    unname(fastLm(Y ~ x, data=.SD)$coefficients))
    ,by = "user_id"]
})
#   user  system elapsed 
#356.604   0.047 356.820

Ta prosta zmiana daje mniej więcej6,5 x speedup .


[oryginalne podejście]

Prawdopodobnie jest miejsce na poprawę, ale procesor 2,6 GHz z procesorem 64-bitowym R zajął około 25 minut na Linuksowej maszynie wirtualnej (procesor 2,6 GHz).]}

library(data.table)
library(RcppArmadillo)
##
dt[
  ,c("lm_b0","lm_b1") := as.list(
    unname(fastLm(Y ~ x, data=.SD)$coefficients)),
  by=user_id]
##
R> dt[c(1:2, 31:32, 61:62),]
   user_id   x         Y     lm_b0    lm_b1
1:       1 1.0 1674.8316 -202.0066 744.6252
2:       1 1.5  369.8608 -202.0066 744.6252
3:       2 1.0  463.7460 -144.2961 374.1995
4:       2 1.5  412.7422 -144.2961 374.1995
5:       3 1.0  513.0996  217.6442 261.0022
6:       3 1.5 1140.2766  217.6442 261.0022

Data:

dt <- data.table(
  user_id = rep(1:500000,each=30))
##
dt[, x := seq(1, by=.5, length.out=30), by = user_id]
dt[, Y := 1000*runif(1)*x, by = user_id]
dt[, Y := Y + rnorm(
  30, 
  mean = sample(c(-.05,0,0.5)*mean(Y),1), 
  sd = mean(Y)*.25), 
  by = user_id]
 8
Author: nrussell,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-12-02 13:40:58

Możesz spróbować użyć danych.stół jak ten. Właśnie stworzyłem dane o zabawkach, ale wyobrażam sobie DANE.tabela dałaby pewną poprawę. Jest dość szybki. Ale jest to dość duży zestaw danych, więc być może porównaj to podejście do mniejszej próbki, aby sprawdzić, czy prędkość jest o wiele lepsza. powodzenia.


    library(data.table)

    exp <- data.table(id = rep(c("a","b","c"), each = 20), x = rnorm(60,5,1.5), y = rnorm(60,2,.2))
    # edit: it might also help to set a key on id with such a large data-set
    # with the toy example it would make no diff of course
    exp <- setkey(exp,id)
    # the nuts and bolts of the data.table part of the answer
    result <- exp[, as.list(coef(lm(y ~ x))), by=id]
    result
       id (Intercept)            x
    1:  a    2.013548 -0.008175644
    2:  b    2.084167 -0.010023549
    3:  c    1.907410  0.015823088
 6
Author: miles2know,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-01-14 06:02:41