2017-09-20 3 views
4

Tenir compte du data.table des événements suivants:Trouver les matchs les plus proches de chaque ligne et somme sur la base d'une condition

library(data.table) 
breaks <- data.table(id = 1:8, 
        Channel = c("NP1", "NP1", "NP2", "NP2", "NP3", "NP3", "AT4", "AT4"), 
        Time = c(1000, 1100, 975, 1075, 1010, 1080, 1000, 1050), 
        Day = c(1, 1, 1, 1, 1, 1, 1, 1), 
        ZA = c(15, 12, 4, 2, 1, 2, 23, 18), 
        stringsAsFactors = F) 

breaks 
    id Channel Time Day ZA 
1: 1  NP1 1000 1 15 
2: 2  NP1 1100 1 12 
3: 3  NP2 975 1 4 
4: 4  NP2 1075 1 2 
5: 5  NP3 1010 1 1 
6: 6  NP3 1080 1 2 
7: 7  AT4 1000 1 23 
8: 8  AT4 1050 1 18 

Pour chaque événement unique dans les pauses, je veux trouver les événements les plus proches dans tous les autres canaux en utilisant la TimeDay == Day puis additionner les valeurs de ZA pour ces événements.

Ceci est le résultat que je veux atteindre:

id Channel Time Day ZA Sum 
1: 1  NP1 1000 1 15 28 
2: 2  NP1 1100 1 12 22 
3: 3  NP2 975 1 4 39 
4: 4  NP2 1075 1 2 32 
5: 5  NP3 1010 1 1 42 
6: 6  NP3 1080 1 2 32 
7: 7  AT4 1000 1 23 20 
8: 8  AT4 1050 1 18 19 

Donc, pour la première ligne du canal est NP1. Les événements se ferme tous les autres canaux Time = 1000 sont les lignes 3, 5 et 7. 4+1+23 = 28

J'ai eu que cela fonctionne en utilisant data.table avec le code suivant:

breaks[breaks[, c("Day", "Time", "Channel", "ZA")], on = "Day", allow.cartesian = TRUE][ 
    Channel != i.Channel][ 
    order(id)][ 
     , delta := abs(Time - i.Time)][ 
     , .SD[delta == min(delta)], by = .(Channel, Time, Day, i.Channel)][ 
      , unique(.SD, by = c("id", "i.Channel"))][ 
      , .(Sum = sum(i.ZA)), by = .(id, Channel, Time, Day, ZA)] 

Cependant, cela crée un ensemble de données avec 64 lignes dans la première étape et je voudrais le faire avec un ensemble de données de plus d'un million de lignes.

Quelqu'un peut-il m'aider à trouver un moyen plus efficace de le faire?

Edit:

J'ai essayé les solutions de G. Grothendieck (sqldf), Eddi (data.table) et MarkusN (dplyr) une fois sur l'ensemble des données de 1,4 millions de lignes avec 39 canaux différents. L'ensemble de données était en mémoire.

sqldf:  54 minutes 
data.table: 11 hours 
dplyr:  29 hours 

Répondre

2

Dans la sélection de chaque auto-jointure interne ligne dans des pauses pour les lignes le même jour et canal différent, puis parmi toutes les lignes jointes à une ligne originale particulière garder seule la ligne jointe ayant la différence de temps absolue minmum. Dans la sélection externe, additionnez le ZA de l'autre canal dans l'identifiant donnant le résultat. Notez que nous supposons ici que le backend SQLite par défaut est sqldf et que nous utilisons une fonctionnalité spécifique à cette base de données, à savoir que si min est utilisé dans une sélection, les autres valeurs spécifiées dans cette sélection seront également renseignées à partir du ligne de minimisation.

Par défaut, il utilisera une base de données en mémoire qui conviendrait le mieux, mais si vous spécifiez dbname = tempfile() comme argument à sqldf, il utilisera un fichier en tant que base de données de mémoire insuffisante. Il serait également possible d'ajouter un ou plusieurs index qui peuvent ou non l'accélérer. Voir la page d'accueil de sqldf github pour plus d'exemples.

library(sqldf) 

sqldf("select id, Channel, Time, Day, ZA, sum(bZA) Sum 
from (
    select a.*, b.ZA bZA, min(abs(a.Time - b.Time)) 
    from breaks a join breaks b on a.Day = b.Day and a.Channel != b.Channel 
    group by a.id, b.Channel) 
group by id") 

donnant:

id Channel Time Day ZA Sum 
1 1  NP1 1000 1 15 28 
2 2  NP1 1100 1 12 22 
3 3  NP2 975 1 4 39 
4 4  NP2 1075 1 2 32 
5 5  NP3 1010 1 1 42 
6 6  NP3 1080 1 2 32 
7 7  AT4 1000 1 23 20 
8 8  AT4 1050 1 18 19 

Ce chiffre est légèrement plus rapide que le code data.table dans la question sur un problème de cette taille, mais pour des problèmes plus la comparaison devrait être refaite à neuf. De plus, il peut être capable de gérer des tailles plus grandes du fait de ne pas avoir à matérialiser les résultats d'intermédation (en fonction de l'optimiseur de requête) et la possibilité de le gérer en mémoire (si besoin est).

library(data.table) 
library(dplyr) 
library(sqldf) 
library(rbenchmark) 

benchmark(sqldf = 
sqldf("select id, Channel, Time, Day, ZA, sum(bZA) Sum 
from (
    select a.*, b.ZA bZA, min(abs(a.Time - b.Time)) 
    from breaks a join breaks b on a.Day = b.Day and a.Channel != b.Channel 
    group by a.id, b.Channel) 
group by id"), 

data.table = breaks[breaks[, c("Day", "Time", "Channel", "ZA")], on = "Day", 
    allow.cartesian = TRUE][ 
    Channel != i.Channel][ 
    order(id)][ 
     , delta := abs(Time - i.Time)][ 
     , .SD[delta == min(delta)], by = .(Channel, Time, Day, i.Channel)][ 
      , unique(.SD, by = c("id", "i.Channel"))][ 
      , .(Sum = sum(i.ZA)), by = .(id, Channel, Time, Day, ZA)], 

dplyr = { breaks %>% 
    inner_join(breaks, by=c("Day"), suffix=c("",".y")) %>% 
    filter(Channel != Channel.y) %>% 
    group_by(id, Channel, Time, Day, ZA, Channel.y) %>% 
    arrange(abs(Time - Time.y)) %>% 
    filter(row_number()==1) %>% 
    group_by(id, Channel, Time, Day, ZA) %>% 
    summarise(Sum=sum(ZA.y)) %>%       
    ungroup() %>% 
    select(id:Sum) }, 

order = "elapsed")[1:4] 

offrant:

 test replications elapsed relative 
1  sqldf   100 3.38 1.000 
2 data.table   100 4.05 1.198 
3  dplyr   100 9.23 2.731 
0

Voici une solution en utilisant dplyr et autojointure:

library(dplyr) 
breaks %>% 
    inner_join(breaks, by=c("Day"), suffix=c("",".y")) %>% # self-join 
    filter(Channel != Channel.y) %>%      # ignore events of same channel 
    group_by(id, Channel, Time, Day, ZA, Channel.y) %>%  # build group for every event 
    arrange(abs(Time - Time.y)) %>%       # sort by minimal time-diff 
    filter(row_number()==1) %>%        # keep just row with minimal time-diff 
    group_by(id, Channel, Time, Day, ZA) %>%    # group by all columns of original event 
    summarise(Sum=sum(ZA.y)) %>%       # sum ZA of other channels 
    ungroup() %>% 
    select(id:Sum) 

Peut-être que je dois être plus précis sur ma réponse. Contrairement à data.table, dplyr a le droit de traduire le code en sql. Ainsi, si vos données sont stockées dans une base de données, vous pouvez vous connecter directement à la table contenant vos données. Tout (la plus grande partie) du code dpylr est évalué dans votre SGBD. Comme effectuer des jointures est une tâche clé de chaque SGBD, vous n'avez pas à vous soucier des performances. Cependant, si vos données sont importées dans R et que vous vous inquiétez des limites de RAM, vous devez parcourir chaque ligne de la base de données. Cela peut être accomlished avec dplyr ainsi:

library(dplyr) 
breaks %>% 
rowwise() %>% 
do({ 
    row = as_data_frame(.) 
    df = 
    breaks %>% 
    filter(Day == row$Day & Channel != row$Channel) %>% 
    mutate(time_diff = abs(Time-row$Time)) %>% 
    group_by(Channel) %>% 
    arrange(abs(Time-row$Time), .by_group=TRUE) %>% 
    filter(row_number()==1) %>% 
    ungroup() %>% summarise(sum(ZA)) 

    row %>% mutate(sumZA = df[[1]]) 
}) 
+0

Ne résout pas le problème d'OP - vous faites exactement la même chose qu'ils ont fait. – eddi

+0

Je pense que c'est une réponse valable à la question et il est utile de comparer plusieurs approches. Il donne la même réponse que celle de la question. –

+0

@ G.Grothendieck Non, ce n'est certainement pas le cas. La préoccupation d'OP est de se joindre à lui-même sur des données d'un million de lignes, et cette réponse ne touche pas à cela. – eddi

2

Je ne suis pas sûr de la vitesse de ce (probablement lent), mais il sera très prudent mémoire sage:

Channels = breaks[, unique(Channel)] 
breaks[, Sum := breaks[breaks[row, 
           .(Day, Channel = setdiff(Channels, Channel), Time)], 
         on = .(Day, Channel, Time), roll = 'nearest', 
         sum(ZA)] 
     , by = .(row = 1:nrow(breaks))] 

Il ll aidera probablement la vitesse à setkey(breaks, Day, Channel, Time) au lieu d'utiliser on.